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 — for example,
also implementing __call__ to work as a decorator via
contextlib.ContextDecorator.
@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.