Errors are objects that propagate up the call stack until something handles them. try / except catches them, raise creates them, and finally (or with) guarantees cleanup.
How an exception travels
Raised
Something fails (
int('x')) or your code runsraise SomeError(...).Unwinds
Python abandons the current function and looks for a matching
exceptin each caller, innermost first.Handled — or not
The first matching
exceptruns and execution continues after itstrystatement. With no match, the traceback is printed and the interpreter exits with status 1.
def level3():
return int("not a number") # raises ValueError here
def level2():
return level3() # no handler → passes through
def level1():
try:
return level2()
except ValueError:
return "handled in level1"
print(level1()) # handled in level1try / except / else / finally
def parse(value):
try:
n = int(value)
except ValueError as e:
return f"bad input: {e}"
else:
return n * 2 # runs only if no exception
finally:
pass # always runs: close files, release locks
print(parse("21")) # 42
print(parse("x")) # bad input: invalid literal for int() with base 10: 'x'| Block | Runs when |
|---|---|
try | Always — the code that might fail |
except E | An exception of type E (or a subclass) was raised |
else | The try block finished without an exception |
finally | Always, even after return, break or an unhandled error |
Raising & custom exceptions
class InsufficientFunds(Exception):
def __init__(self, needed):
super().__init__(f"need {needed} more")
self.needed = needed
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFunds(amount - balance)
return balance - amount
try:
withdraw(100, 150)
except InsufficientFunds as e:
print(e, e.needed) # need 50 more 50
try:
try:
{}["k"]
except KeyError as e:
raise LookupError("config missing") from e # keep the original cause
except LookupError as e:
print(repr(e.__cause__)) # KeyError('k')Exception hierarchy (the useful part)
| Exception | Typical cause |
|---|---|
ValueError | Right type, wrong value: int('x') |
TypeError | Wrong type: 'a' + 1 |
KeyError / IndexError | Missing dict key / list index out of range (both are LookupErrors) |
AttributeError | Object has no such attribute |
NameError | Name not defined (typo, or used before assignment) |
ZeroDivisionError | Division by zero (an ArithmeticError) |
FileNotFoundError | Missing file (an OSError) |
StopIteration | Iterator is exhausted |
RecursionError | Recursion deeper than sys.getrecursionlimit() |
The full tree, BaseException vs Exception, exception groups and chaining are in Exception Hierarchy & Advanced Handling.
Guaranteed cleanup: finally and with
from contextlib import suppress
events = []
def risky():
try:
events.append("open")
raise RuntimeError("boom")
finally:
events.append("close") # runs even though the error escapes
try:
risky()
except RuntimeError:
pass
print(events) # ['open', 'close']
with suppress(FileNotFoundError): # ignore one specific, expected error
open("definitely-missing.txt")
print("still running") # still runningWhat is the difference between else and finally in a try statement?