Use context managers for reliable cleanup¶
The question. You're acquiring a resource — a file, a database connection, a lock, a temporary directory — and you need to guarantee it gets released when you're done, even if an exception is raised in between. Writing a try/finally for each one is verbose and easy to forget.
The answer: use a context manager with the with statement. The canonical one is open(); the @contextmanager decorator lets you build your own in a few lines whenever the built-in one doesn't fit.
# The canonical pattern: open() is a context manager.
# The 'with' block guarantees f.close() runs — on success, on exception, on early return.
from pathlib import Path
path = Path('/tmp/ctx-demo.txt')
path.write_text('First line\nSecond line\nThird line\n')
# Cleanup-guaranteed form:
with open(path, encoding='utf-8') as f:
content = f.read()
# f is now closed — no matter how we left the block.
print(f'read {len(content)} chars')
print(f'file closed: {f.closed}')
# Multiple resources in one 'with' (Python 3.10+):
dest = Path('/tmp/ctx-demo-copy.txt')
with (
open(path, encoding='utf-8') as src,
open(dest, 'w', encoding='utf-8') as dst,
):
dst.write(src.read().upper())
print(dest.read_text())
Variant: write one with @contextmanager¶
For simple setup/cleanup, a generator is the shortest path. yield splits the function into 'before' (setup) and 'after' (cleanup). Wrap the yield in try/finally so cleanup runs even if the body raises.
import time
from contextlib import contextmanager
@contextmanager
def timer(label='operation'):
'''Measure and print the wall-clock time of the with-block.'''
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f'[{label}] {elapsed:.4f}s')
with timer('list comp'):
squares = [x * x for x in range(100_000)]
Variant: a class for stateful context managers¶
When you want the manager's object to survive the block — so you can inspect its state afterwards, say — write a class. __enter__ returns self; __exit__ sets attributes the caller reads later.
import time
class Timer:
def __init__(self, label: str = 'operation') -> None:
self.label = label
self.elapsed: float = 0.0
def __enter__(self) -> 'Timer':
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
self.elapsed = time.perf_counter() - self._start
print(f'[{self.label}] {self.elapsed:.4f}s')
return False # don't suppress exceptions
with Timer('sum') as t:
total = sum(range(1_000_000))
print(f'elapsed was {t.elapsed:.4f}s; total = {total}')
Variant: suppress expected exceptions with contextlib.suppress¶
When 'the resource isn't there and that's fine', contextlib.suppress(ExceptionType) is clearer than a bare except: pass. Use for the narrow cases only — not as a general swallow.
from contextlib import suppress
from pathlib import Path
# Delete if present, ignore if not — idempotent cleanup.
with suppress(FileNotFoundError):
Path('/tmp/does-not-exist-and-that-is-fine.txt').unlink()
print('continued without error')
Why this works¶
A context manager is any object with __enter__ and __exit__ methods. with calls __enter__, runs the block, and calls __exit__ on the way out — whether the block finishes normally or raises. __exit__ gets the exception info if one occurred, and can suppress it by returning True (though you rarely should).
The shape is equivalent to try/finally, but with the cleanup code written next to the acquisition code instead of wherever the exit happens. That's why the pattern scales to multiple resources: each manager's __exit__ runs in reverse order, so resources are released in the inverse order they were acquired — the standard LIFO discipline for nested resources.
When built-in managers don't cover your case, the @contextmanager decorator lets you write one as a generator: setup before yield, cleanup in a finally after. It's the lightweight path; a full class is the heavier but more flexible option.
Trade-offs¶
Reach for @contextmanager (see extra cells) when the setup/cleanup is linear and stateless — a timer, a temp directory, a database transaction. Reach for a class when you need to store state between __enter__ and __exit__ (a Timer whose .elapsed you'll inspect after), or when the context-manager behaviour is part of an object's broader lifecycle.
__exit__ returning True suppresses the exception. Do this sparingly — it's the silent-swallow anti-pattern in disguise. contextlib.suppress(FileNotFoundError) is a clean built-in for the one common case: 'try this, ignore if the resource isn't there'.
Multiple context managers in one with statement compose cleanly (Python 3.10+ supports the parenthesised form across lines). If any __enter__ raises, the ones that already entered are exited in reverse order — there's no leak window.
Related reading¶
- Avoid common error handling mistakes — the 'forget to close' anti-pattern.
- Handle multiple exceptions — what
__exit__sees, and why returningTrueis a tool to use sparingly. contextlib—suppress,closing,ExitStack, and friends in the standard library.