Errors and exceptions

Things go wrong in every program: a user types letters where a number is expected, a file is missing, a list is shorter than assumed. Python distinguishes two kinds of errors, and they happen at different moments.

Syntax errors

A syntax error means the code breaks the grammar of the language — a missing parenthesis, a missing colon, wrong indentation. Python finds it while compiling the file, before the first line runs. So in the example below, not even "Start" is printed:

print("Start")
print("End"
SyntaxError: '(' was never closed

A syntax error cannot be caught by the code that contains it; you have to fix the code.

Exceptions at run time

Code that is syntactically correct can still fail while it runs. When an operation cannot be carried out, Python raises an exception: an object that describes the error and interrupts the normal flow of the program. If nothing handles it, the program stops at that line.

print("Start")
print(10 / 0)
print("End")                  # never reached
Start
ZeroDivisionError: division by zero

Some exceptions you will meet often:

  • ValueError — the type is right but the value is not: int("twelve").
  • TypeError — an operation on the wrong type: "3" + 4.
  • NameError — a name that is not defined.
  • IndexError — a list index outside the list; KeyError — a dictionary key that does not exist.
  • ZeroDivisionError — division or modulo by zero.
  • FileNotFoundError — opening a file that does not exist.

Reading a traceback

When an exception is not handled, Python prints a traceback: a report of the chain of function calls that led to the error. Suppose the file stats.py contains:

def average(values):
    return sum(values) / len(values)

readings = []
print(average(readings))

Running it prints:

Traceback (most recent call last):
  File "/home/ada/stats.py", line 5, in <module>
    print(average(readings))
          ^^^^^^^^^^^^^^^^^
  File "/home/ada/stats.py", line 2, in average
    return sum(values) / len(values)
           ~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero

Read it from the bottom up:

  1. The last line is the most important: the type of the exception (ZeroDivisionError) and its message (division by zero).
  2. The entry just above it is where it happened: line 2, inside the function average. The markers point at the exact operation, here the /.
  3. Entries higher up show how the program got there: line 5 of the main program (<module>) called average(). "Most recent call last" means the calls are listed in the order they were made.

The line where the error appears is not always where the mistake is. Here the division is correct; the real problem is that average() received an empty list.

Handling exceptions with try and except

The try statement lets you handle an exception instead of letting it stop the program. Put the code that might fail in the try block, and the code that deals with the failure in an except block.

for text in ["42", "12.5"]:
    try:
        quantity = int(text)
        print("Quantity:", quantity)
    except ValueError:
        print("Not a whole number:", text)
print("Program continues")
Quantity: 42
Not a whole number: 12.5
Program continues

This is what happens:

  1. Python runs the try block.
  2. If no exception occurs, all except blocks are skipped.
  3. If an exception occurs, the rest of the try block is skipped (print("Quantity:", …) does not run for "12.5"), and Python looks for an except clause that matches the exception's type.
  4. If one matches, its block runs and the program continues after the whole try statement (after any finally block has run). If none matches, the exception travels on as if there were no try.

Catching specific exceptions

Name the exceptions you expect and know how to handle. A try statement can have several except clauses, and one clause can catch several types by listing them in a tuple. At most one except block runs: the first one that matches. Python 3.14 and later also accept this without parentheses when there is no as; older versions and exam questions use the tuple form.

def price_per_item(total, count):
    try:
        return round(float(total) / int(count), 2)
    except ZeroDivisionError:
        return "no items"
    except (ValueError, TypeError):
        return "invalid input"

print(price_per_item("9.60", "3"))
print(price_per_item("9.60", "0"))
print(price_per_item("nine", "3"))
print(price_per_item(None, "3"))
3.2
no items
invalid input
invalid input

The exception object: as

Write except SomeError as e: to bind the exception object to a name. Converting it to a string gives its message; type(e).__name__ gives the name of its class. The name e is removed again at the end of the except block.

stock = {"tea": 12, "coffee": 0}

try:
    print(int("twelve"))
except ValueError as e:
    print("Error:", e)

try:
    print(stock["cake"])
except KeyError as e:
    print(type(e).__name__, e)
Error: invalid literal for int() with base 10: 'twelve'
KeyError 'cake'

else and finally

A try statement can have two more clauses, always in this order: try, except, else, finally.

  • else runs only if the try block raised no exception. Code there is not protected by the except clauses, which keeps the try block small: it contains only the line that can fail.
  • finally runs always: after success, after a handled exception, after an unhandled one, and even when the block is left with return. It is the place for clean-up, such as closing a connection.
def read_price(text):
    try:
        price = float(text)
    except ValueError:
        print(f"{text!r} is not a number")
    else:
        print(f"price: {price:.2f}")
    finally:
        print("checked", text)

read_price("3.2")
read_price("free")
price: 3.20
checked 3.2
'free' is not a number
checked free
def connect():
    try:
        print("connecting")
        return "connected"
    finally:
        print("clean-up runs before the function returns")

print(connect())
connecting
clean-up runs before the function returns
connected

The exception hierarchy

Exceptions are classes, and they are organized in a family tree: the exception hierarchy. Every exception class inherits from a more general one. An except clause for a class also catches all of its subclasses. Part of the tree of built-in exceptions:

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 └── Exception
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── OSError
      │    └── FileNotFoundError
      ├── NameError
      │    └── UnboundLocalError
      ├── TypeError
      ├── ValueError
      └── AssertionError

BaseException is the root. Two of its direct subclasses, SystemExit and KeyboardInterrupt, are not really errors — they are how a program is asked to stop — so they sit outside Exception. That is why except Exception: does not block Ctrl+C. You can inspect the tree in code:

print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(KeyError, LookupError))
print(issubclass(KeyboardInterrupt, Exception))

readings = [14, 17]
try:
    print(readings[5])
except LookupError as e:      # catches IndexError and KeyError
    print("Lookup failed:", type(e).__name__)
True
True
False
Lookup failed: IndexError

Order matters: specific first

Python tests the except clauses from top to bottom and runs the first that matches. If a general class comes first, it also matches its subclasses, and a more specific clause below it can never run:

try:
    print(10 / 0)
except ArithmeticError:
    print("ArithmeticError handler")
except ZeroDivisionError:
    print("ZeroDivisionError handler")     # never reached
ArithmeticError handler

Python does not warn about this. Always put the most specific exceptions first and the most general ones last.

Raising exceptions

Your own code can signal an error, too. The raise statement raises an exception: raise ValueError("message") creates an exception object and throws it. Raise an exception when a function receives input it cannot work with; it is better than returning a special value that the caller might forget to check.

def set_thermostat(celsius):
    if celsius < 5 or celsius > 30:
        raise ValueError(f"temperature out of range: {celsius}")
    print("Thermostat set to", celsius)

set_thermostat(21)
set_thermostat(45)
Thermostat set to 21
ValueError: temperature out of range: 45

The caller decides what to do about it, with its own try statement:

def set_thermostat(celsius):
    if celsius < 5 or celsius > 30:
        raise ValueError(f"temperature out of range: {celsius}")
    print("Thermostat set to", celsius)

for wanted in [19, 45]:
    try:
        set_thermostat(wanted)
    except ValueError as e:
        print("Refused:", e)
Thermostat set to 19
Refused: temperature out of range: 45

Re-raising an exception

Inside an except block, a bare raise raises the same exception again. Use it when you want to react to an error — log it, for example — but cannot fully handle it, so it must still reach the caller.

def parse_quantity(text):
    try:
        return int(text)
    except ValueError:
        print("log: could not parse", repr(text))
        raise

print(parse_quantity("42"))
parse_quantity("forty")
42
log: could not parse 'forty'
ValueError: invalid literal for int() with base 10: 'forty'

To replace an exception with a different one while keeping the original as the cause, write raise NewError("…") from e. from e marks the original exception as the direct cause; the traceback shows both, linked by the line "The above exception was the direct cause of the following exception".

Custom exception classes

For errors specific to your program, define your own exception class that inherits from Exception. The class body can be just a docstring. A named exception type lets callers catch exactly your error and nothing else. Classes and inheritance are explained in Object-Oriented Programming Basics.

class InsufficientFundsError(Exception):
    """Raised when a withdrawal is larger than the balance."""

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(f"balance {balance}, requested {amount}")
    return balance - amount

print(withdraw(100, 30))
try:
    withdraw(50, 80)
except InsufficientFundsError as e:
    print("Refused:", e)
70
Refused: balance 50, requested 80

assert

The assert statement checks a condition that should always be true if your code is correct. If the condition is false, it raises an AssertionError, with an optional message.

def average(values):
    assert len(values) > 0, "values must not be empty"
    return sum(values) / len(values)

print(average([3, 4, 5]))
average([])
4.0
AssertionError: values must not be empty

pytest builds on this statement: a test checks its results with a plain assert, and pytest shows both sides when it fails (see Unit Testing with pytest).

EAFP and LBYL

There are two styles of dealing with situations that might fail.

  • LBYL, "look before you leap": check first, then act — if key in stock: before reading stock[key].
  • EAFP, "easier to ask forgiveness than permission": just do it, and handle the exception if it fails.
stock = {"tea": 12, "coffee": 0}

# LBYL: look before you leap
if "cake" in stock:
    print(stock["cake"])
else:
    print("cake: not sold here")

# EAFP: easier to ask forgiveness than permission
try:
    print(stock["cake"])
except KeyError:
    print("cake: not sold here")
cake: not sold here
cake: not sold here

Python code usually prefers EAFP. The check and the action cannot drift apart, and it avoids a subtle problem: between checking that a file exists and opening it, another program may delete it. With EAFP you simply try to open the file and handle FileNotFoundError — see Files and Paths. LBYL is fine when the check is cheap and failure is a normal, expected case; for dictionaries, stock.get("cake", 0) is often simplest of all.