How do you write a context manager using `contextlib.contextmanager`?

6 minintermediatecontext-managerscontextlib

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.