Python · Intermediate

Exceptions and Error Handling in Python

10 min readUpdated September 24, 2026Every example verified

In short: An exception is an object Python raises when something goes wrong; it unwinds the call stack until a matching except clause catches it. try/except handles expected failures such as bad input or a missing file, raise signals a problem from your own code, and finally runs cleanup whether or not an error occurred.

How exceptions work and how to handle them

When Python cannot continue, it raises an exception: dividing by zero, converting "eight" to an integer, reading a missing key. The exception travels up through the calling functions until something catches it; if nothing does, the program stops with a traceback. Exceptions are how Python reports failure, and handling them is how a program turns a crash into a message, a retry or a fallback.

A try block wraps the code that might fail; an except clause names the exception type it handles and receives the exception object with as err. Name the specific exception: except ValueError catches a bad conversion and nothing else, so a genuine bug elsewhere in the block still surfaces with its traceback.

Two optional clauses complete the statement. else runs only if the try block raised nothing, which keeps the success path out of the protected region so its own errors are not mistaken for the one you expected. finally runs no matter what, whether the block succeeded, raised or returned, which makes it the place for cleanup such as closing a connection. Several except clauses can follow one try; the first whose type matches wins, and a tuple such as except (TypeError, ValueError) catches either.

Exceptions form a class hierarchy. ValueError, KeyError, ZeroDivisionError and FileNotFoundError all descend from Exception, and an except clause matches an exception of the named class or any subclass. LookupError therefore catches both KeyError and IndexError, and OSError catches every file and network failure.

Your own code raises with raise SomeError("message"). Use the built-in type that fits (ValueError for a bad argument, TypeError for a wrong kind) or define one by subclassing Exception, which lets callers catch your code's failures precisely. Inside an except block, a bare raise re-raises the current exception after you have logged it, and raise NewError(...) from err records the original as the cause.

Python's style is to try the operation and handle the failure rather than test every precondition first. Opening a file and catching FileNotFoundError is shorter and safer than checking existence beforehand, because the file can vanish in between. That said, do not use exceptions where an ordinary check reads better: if key in d is clearer than catching KeyError for a routine lookup.

Syntax

 Python · syntax
try:
    risky()
except ValueError as err:          # one type, with the exception object
    print("bad value:", err)
except (TypeError, KeyError):      # several types in one clause
    ...
else:                              # only if no exception was raised
    ...
finally:                           # always, even after return or an unhandled error
    ...

raise ValueError("amount must be positive")

class InsufficientStock(Exception):
    pass

Order the except clauses from most specific to most general; a clause for Exception placed first would catch everything and hide the later ones.

Catching a ValueError from bad input

Quantities are read one per line. Lines that are not whole numbers are reported and skipped instead of stopping the program.

 Python
count = int(input())
total = 0
skipped = 0
for _ in range(count):
    text = input().strip()
    try:
        total += int(text)
    except ValueError:
        print(f"skipping {text!r}: not a whole number")
        skipped += 1
print("total:", total)
print("skipped:", skipped)

Input given to the program: 512eight7.5203

Output

skipping 'eight': not a whole number
skipping '7.5': not a whole number
total: 35
skipped: 2

int("eight") and int("7.5") both raise ValueError, which the except clause turns into a message. Because the try block contains only the conversion, the handler cannot accidentally catch an unrelated error from elsewhere in the loop. The !r in the f-string prints the offending text with quotes so a stray space would be visible.

Multiple except clauses, else and finally

One function called three times: a success, a division by zero and a wrong type. Watch which clauses run in each case.

 Python
def share(amount, people):
    try:
        each = amount / people
    except ZeroDivisionError:
        print("nobody to share with")
    except TypeError as err:
        print("bad input:", err)
    else:
        print(f"each gets {each:.2f}")
    finally:
        print("done")

share(90, 4)
share(90, 0)
share("90", 4)

Output

each gets 22.50
done
nobody to share with
done
bad input: unsupported operand type(s) for /: 'str' and 'int'
done

The first call raises nothing, so else prints the result. The second raises ZeroDivisionError, matched by the first clause; the third raises TypeError, matched by the second, and as err gives access to Python's own message. finally prints done all three times. Putting the result line in else rather than inside try means a formatting bug there would not be mistaken for a division error.

Raising built-in and custom exceptions

A stock-taking function raises KeyError for unknown items and a custom InsufficientStock for shortages; the caller handles each differently.

 Python
class InsufficientStock(Exception):
    pass

def take(stock, item, qty):
    if item not in stock:
        raise KeyError(item)
    if stock[item] < qty:
        raise InsufficientStock(f"only {stock[item]} {item} left, wanted {qty}")
    stock[item] -= qty

stock = {"paint": 5, "brush": 2}
requests = [("paint", 3), ("brush", 4), ("tape", 1), ("paint", 2)]
for item, qty in requests:
    try:
        take(stock, item, qty)
        print(f"took {qty} {item}")
    except InsufficientStock as err:
        print("refused:", err)
    except KeyError as err:
        print("unknown item:", err)
print(stock)

Output

took 3 paint
refused: only 2 brush left, wanted 4
unknown item: 'tape'
took 2 paint
{'paint': 0, 'brush': 2}

The custom class needs no body: inheriting from Exception gives it everything, including the message passed to the constructor, which print shows through str(err). KeyError prints its argument in quotes, which is how Python formats that particular exception. Because each request is handled inside the loop, one refusal does not stop the others, and the final stock reflects only the successful requests.

Common built-in exceptions

ExceptionRaised whenTypical trigger
ValueErrora value has the right type but unusable contentint("abc")
TypeErroran operation receives the wrong type"a" + 1
KeyErrora dictionary key is missingd["absent"]
IndexErrora sequence index is out of range[1, 2][5]
ZeroDivisionErrordivision or modulo by zerox / 0
FileNotFoundErrora file to read does not existopen("missing.txt")
AttributeErroran object has no such attribute or methodNone.upper()

Common mistakes

  • Using a bare except:

    Why it goes wrong: It catches everything, including KeyboardInterrupt, SystemExit and the bugs you did not know about, so the program hides its own errors and cannot be stopped cleanly.

    Fix: Catch the specific type you expect, or at the very least except Exception as err: and report err.

  • Wrapping too much code in one try

    Why it goes wrong: If the try block has twenty lines, an except ValueError may catch a ValueError from a line you never meant to protect, and the handler's message will be wrong.

    Fix: Keep the try block to the one or two statements that can fail; put the follow-up code in else.

     Python · fix
    try:
        qty = int(text)
    except ValueError:
        qty = 0
    else:
        process(qty)
  • Catching an exception and doing nothing

    Why it goes wrong: except ValueError: pass makes the failure invisible. The program continues with wrong data and the real problem shows up somewhere far away.

    Fix: Handle it meaningfully: report it, substitute a default, or re-raise with a bare raise.

  • Raising a generic Exception for everything

    Why it goes wrong: raise Exception("bad input") forces every caller to catch Exception, which also catches unrelated bugs, so nobody can handle your error precisely.

    Fix: Raise the matching built-in (ValueError, TypeError) or a small custom class derived from Exception.

Where you use this

Input validation is the everyday case. A program that reads numbers, dates or file names from a user, a form or a file cannot assume they are well formed; wrapping each conversion in try/except ValueError lets it report the bad line and carry on instead of dying on the first typo. The same shape protects network calls and file access, where the failure is outside your control.

Custom exceptions earn their place in code other people call. A payment module that raises InsufficientFunds and CardDeclined lets the caller handle those two cases differently while letting an unexpected TypeError propagate as the bug it is. The finally clause keeps resources tidy: a temporary file removed, a lock released, a progress message printed, whether the work succeeded or not.

 Python · in practice
try:
    with open(path, encoding="utf-8") as f:
        data = f.read()
except FileNotFoundError:
    data = ""   # first run: nothing saved yet

Key points

  • Exceptions are objects raised on failure; an uncaught one stops the program with a traceback.
  • Catch specific types such as except ValueError, never a bare except:.
  • else runs only on success; finally always runs, so it is the place for cleanup.
  • An except clause matches the named class and all its subclasses; Exception is the base of the ordinary ones.
  • Raise built-in types for ordinary misuse and small custom subclasses for errors specific to your code.
  • Keep the try block small and never silence an exception without a reason.

Try it yourself

The program converts the line it reads to an integer and prints double the value. Add a try/except so that a line that is not a whole number prints not a number instead of crashing.

Your program
text = input().strip()
value = int(text)
print(value * 2)
Input the program receives: forty
Expected output: not a number

Practise this

Open the Python playground

Frequently asked questions

What is the difference between except Exception and a bare except?

A bare except: catches every exception including KeyboardInterrupt and SystemExit, which derive from BaseException rather than Exception, so it can stop a program from exiting when the user presses Ctrl+C. except Exception leaves those alone but still catches every ordinary error. Both are too broad for most handlers; name the specific type when you can.

When does the finally block run?

Always: after the try block finishes normally, after an except clause handles an exception, when an exception is not handled and continues upward, and even when the try block executes return, break or continue. That guarantee is why cleanup code belongs there.

Should I check a condition first or catch the exception?

Catch the exception when the check would duplicate the operation or could become stale, as with file existence and network availability, or when the failure is rare. Use a plain check when it reads better and is cheap, such as if key in d before a lookup. Both are idiomatic; pick the one that makes the code clearest.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with CPython 3.11 at build time; runs in your browser on CPython 3.14 (Pyodide) by the publishing checks, and the output shown is what it printed. The Run buttons execute the same code on your device; nothing you type is sent anywhere.