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:

  1. No exceptiontry runs to completion, except is skipped, finally runs.
  2. Exception caughttry partially runs until the error, except runs, then finally runs.
  3. Exception not caughttry partially runs, no except matches, finally runs, 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 with block here (next lesson). with handles the close-on-error pattern automatically. finally is the more general tool that powers with underneath.

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.

Toggle theme (T)