Python context managers, explained
with open(...) as f: is the first context manager every Python developer meets, but the
pattern goes far beyond files. A context manager guarantees that setup and teardown
happen reliably, even when an exception is raised — which is why interviewers use it to
probe your understanding of resource management and the protocol underneath. This guide
covers both ways to build one and the exception-handling subtleties.
What the with statement does
A with block calls an object's __enter__ on the way in and its __exit__ on the way
out — guaranteed, whether the block finishes normally, returns, or raises. It's
structured cleanup without a manual try/finally.
with open("data.txt") as f: # __enter__ runs, returns the file
data = f.read()
# __exit__ runs here no matter what — the file is closed
The value after as is whatever __enter__ returns. The win over try/finally is that
the cleanup logic lives with the resource, not scattered around every use site.
The protocol: enter and exit
Any object implementing __enter__ and __exit__ is a context manager. __enter__ does
setup and returns the value bound by as; __exit__(exc_type, exc_val, exc_tb) does
teardown and receives details of any exception that occurred.
class Resource:
def __enter__(self):
print("acquire")
return self # bound to the `as` variable
def __exit__(self, exc_type, exc_val, exc_tb):
print("release") # always runs
return False # don't suppress exceptions
with Resource() as r:
print("using", r)
# acquire / using / release
If the block raised, the three exc_* arguments describe it; if it exited cleanly, they're
all None.
Handling exceptions in exit
__exit__ is where the resource-management subtlety lives. Its return value decides
whether the exception propagates: return a falsy value (the default) and the exception
re-raises after cleanup; return True and you suppress it.
class Suppress:
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print("swallowing", exc_val)
return True # suppress ValueError only
return False # let everything else propagate
with Suppress():
raise ValueError("boom") # swallowed; program continues
The trap: an accidental return True silently hides every error. Default to returning
False (or nothing) unless suppression is genuinely intended.
The easy way: contextlib.contextmanager
For simple cases, writing a whole class is overkill. The @contextmanager decorator turns
a generator into a context manager: everything before yield is setup, the yielded
value is the as target, and everything after yield is teardown.
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.perf_counter() # setup
try:
yield # the with-block runs here
finally:
print(f"{label}: {time.perf_counter() - start:.4f}s") # teardown
with timer("query"):
run_query()
The try/finally around yield is what makes teardown run even on exception — the
exception is re-raised at the yield inside the generator, so finally catches it.
Managing multiple resources
You can open several context managers in one with, and they're entered left-to-right and
exited in reverse — so dependent resources tear down in the right order.
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
# dst closes first, then src
For a dynamic or unknown number of resources, contextlib.ExitStack lets you enter context
managers in a loop and guarantees they all close.
Where context managers earn their keep
Anywhere there's a paired acquire/release: files (close), locks
(with lock: acquire/release), database connections/transactions (commit or rollback),
temporary directories, and patching in tests.
import threading
lock = threading.Lock()
with lock: # __enter__ acquires, __exit__ releases — even on error
update_shared_state()
The guarantee that release happens even when the body raises is exactly why locks and transactions use this pattern.
Recap
A context manager pairs __enter__ (setup) with __exit__ (teardown), and the
with statement guarantees teardown runs even on exceptions. __exit__'s return value
controls propagation — falsy re-raises (the safe default), True suppresses. For simple
cases, @contextlib.contextmanager turns a generator into a context manager with a
try/finally around yield. Multiple managers exit in reverse order, and ExitStack
handles dynamic sets. Reach for with whenever a resource must be released no matter what
— it's cleaner and safer than hand-rolled try/finally.