The finally clause is code that runs no matter what — whether the try block succeeded, raised an exception that was caught, or raised one that propagated.
It’s the place for cleanup: closing a file, releasing a lock, disconnecting a socket.
The pattern
try:
risky_thing()
except ValueError:
print("caught ValueError")
finally:
print("this always runs")
Three outcomes:
- No exception —
tryruns to completion,exceptis skipped,finallyruns. - Exception caught —
trypartially runs until the error,exceptruns, thenfinallyruns. - Exception not caught —
trypartially runs, noexceptmatches,finallyruns, then the exception propagates up.
When finally is useful
The classic case is anything you opened that should be closed:
file = open("data.txt")
try:
process(file)
finally:
file.close() # runs whether process() succeeded or crashed
Without finally, if process(file) raised an exception, the file would never be closed.
In real code, you’d use a
withblock here (next lesson).withhandles the close-on-error pattern automatically.finallyis the more general tool that powerswithunderneath.
finally runs even on return
This catches people out. A return inside the try doesn’t skip the finally:
def example() -> int:
try:
return 1
finally:
print("still runs")
print(example())
still runs
1
The function returns 1, but finally runs first. The function’s caller still receives 1.
finally runs even on exception that wasn’t caught
try:
raise ValueError("oops")
finally:
print("cleanup")
cleanup
Traceback (most recent call last):
...
ValueError: oops
Cleanup happens, then the exception keeps propagating.
Combining try/except/else/finally
The full form, in order:
try:
might_fail()
except ValueError:
print("recover")
else:
print("succeeded")
finally:
print("cleanup")
Order is fixed: try → optional excepts → optional else → optional finally.
In practice, you’ll rarely use all four. try/except covers most cases; add finally when you need cleanup.
A small example — timing a function
A pattern that pops up in many places — measure how long a piece of code takes, even if it fails:
import time
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def timed(label: str) -> Iterator[None]:
start: float = time.perf_counter()
try:
yield
finally:
elapsed: float = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
This is a peek at writing a context manager (next lesson) — the @contextmanager decorator turns this generator into one — but the finally is what makes the timing report happen even if the operation failed.
What’s next
You can clean up after exceptions. Next, how to raise your own exceptions to signal that something went wrong.