Error Handling
What exceptions are, how to catch them with try, except, else and finally, and how to raise your own.
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:
-
The last line is the most important: the type of
the exception (
ZeroDivisionError) and its message (division by zero). -
The entry just above it is where it happened:
line 2, inside the function
average. The markers point at the exact operation, here the/. -
Entries higher up show how the program got there:
line 5 of the main program (
<module>) calledaverage(). "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:
- Python runs the
tryblock. -
If no exception occurs, all
exceptblocks are skipped. -
If an exception occurs, the rest of the
tryblock is skipped (print("Quantity:", …)does not run for"12.5"), and Python looks for anexceptclause that matches the exception's type. -
If one matches, its block runs and the program continues after the
whole
trystatement (after anyfinallyblock has run). If none matches, the exception travels on as if there were notry.
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.
-
elseruns only if thetryblock raised no exception. Code there is not protected by theexceptclauses, which keeps thetryblock small: it contains only the line that can fail. -
finallyruns always: after success, after a handled exception, after an unhandled one, and even when the block is left withreturn. 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 readingstock[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.