How do you write a context manager using `contextlib.contextmanager`?
Quick Answer
`@contextlib.contextmanager` turns a **generator function** into a context manager: code before the single `yield` runs as `__enter__` (the yielded value becomes the `as` target), and code after `yield` runs as `__exit__` — wrapping the `yield` in `try`/`finally` handles cleanup on exceptions too. It's a much more concise alternative to writing a full class with `__enter__`/`__exit__` for simple setup/teardown logic.
Detailed Answer
The generator-based shortcut
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.perf_counter()
try:
yield start # this becomes the `as` target
finally:
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.3f}s")
with timer() as start_time:
do_expensive_work()
# prints elapsed time whether do_expensive_work() succeeded or raised
Everything before yield is the "enter" logic; the yielded value is
bound to the as variable; everything after yield (in the finally)
is the "exit" logic — the try/finally is what guarantees the cleanup
code runs even if the with block's body raises.
How exceptions flow through the generator
@contextmanager
def suppress_value_errors():
try:
yield
except ValueError:
print("suppressed a ValueError")
# not re-raising -- suppresses it, same as __exit__ returning True
with suppress_value_errors():
raise ValueError("oops")
print("still runs") # exception was suppressed
If the with block raises, that exception is raised at the yield
statement itself inside the generator — so a try/except wrapped
around the yield can catch and optionally suppress it (by not
re-raising), exactly mirroring what returning True from a class-based
__exit__ would do.
Class-based vs @contextmanager: when to choose which
# Class-based -- needed when you must hold state across calls,
# or reuse the context manager as a decorator/multiple times cleanly
class ManagedResource:
def __enter__(self): ...
def __exit__(self, *exc_info): ...
# @contextmanager -- concise, ideal for simple setup/teardown pairs
@contextmanager
def managed_resource():
resource = acquire()
try:
yield resource
finally:
release(resource)
@contextmanager is usually the more concise, more readable choice for
straightforward "acquire, yield, release" logic; a full class is better
when the context manager needs multiple methods, needs to be
instantiated once and reused as a context manager many times without
re-running setup, or needs to double as something else (e.g., also
implementing __call__ to work as a decorator via
contextlib.ContextDecorator).
Interview-ready summary: @contextmanager converts a single-yield
generator into a context manager — code before yield is __enter__,
code after (in a finally) is __exit__, and wrapping yield in
try/except lets you intercept or suppress exceptions from the with
block, exactly mirroring the class-based protocol with far less
boilerplate.