skip to content

How does @contextlib.contextmanager turn a generator function into a context manager?

level: juniorimportance: must knowfreq 70%

answer

  1. One decorator, one generator
  2. Setup, yield, teardown
  3. The yield splits enter from exit
  4. Yielded value is what as binds
  5. Exactly one yield per call

basics

~10 s

contextlib.contextmanager decorates a generator function that yields exactly once. Everything before the yield runs on entry, the yielded value is what the as clause binds, and everything after the yield runs on exit.

solid answer

~40 s

`contextlib.contextmanager` decorates a generator *function*; calling that function returns a context-manager object rather than a bare generator. Entering the `with` advances the generator to its single `yield`, so the code before the yield is the setup a class would put in `__enter__`, and the value yielded is what `as` binds. Leaving the block resumes the generator once more, so the code after the yield is the teardown `__exit__` would perform. The generator must yield exactly once per call: returning without yielding raises `RuntimeError: generator didn't yield`, and a second yield raises `RuntimeError: generator didn't stop`. Because an exception from the body is re-raised at the yield point, teardown belongs in a `finally` rather than after a bare yield.

code

python · 12 lines
python
import contextlib

@contextlib.contextmanager
def tagged(name):
    print("open", name)
    try:
        yield name.upper()
    finally:
        print("close", name)

with tagged("ledger") as handle:
    print("body sees", handle)

go deeper

for a junior

Be ready to write one from memory: decorate a generator, do setup, try: yield, finally: teardown. Know that the yielded value is what the as clause binds and that a bare yield yields None.

for a middle

Explain the mechanics: entry advances the generator to its single yield, exit resumes it, and an exception in the block is re-raised at the yield. Recognise the two RuntimeErrors for zero and two yields.

for a senior

An interviewer expects you to say when this form is the wrong tool — reuse, reentrancy, an object with methods, or inheritance — and to show teardown placed in finally rather than after a bare yield in code you review.

for a principal

Own the convention: decide when a codebase standardises on generator-based managers for short setup/teardown and reserves classes for shared, reusable resources, so that reviewers are not arguing the choice on every pull request.

## The problem it solves A context manager is any object with `__enter__` and `__exit__`. Writing one as a class means a `class`, an `__init__` to stash arguments, an `__enter__` that returns something, and an `__exit__` with a three-argument signature — a lot of ceremony for what is usually "do this, then hand control to the block, then always undo it". `contextlib.contextmanager` lets you write that shape as ordinary straight-line code in a generator, where the `yield` marks the hand-off point. ## The mechanical translation Decorating a generator function with `@contextlib.contextmanager` replaces it with a factory. Calling the decorated function does **not** run any of the body; it creates the underlying generator object and wraps it in a helper object that implements the context-manager protocol. * `with cm_factory(...) as value:` calls the factory, then calls `__enter__` on the wrapper. * `__enter__` advances the generator to its first `yield`. Every statement before the `yield` therefore runs as setup, and whatever the `yield` produces becomes the `__enter__` return value — the thing `as` binds. * When the block finishes normally, `__exit__` resumes the generator. The statements after the `yield` run as teardown, and the generator is expected to finish (fall off the end or `return`). * When the block raises, the exception is re-raised *inside the generator, at the `yield` expression*, so the generator can see it with `except`, clean up with `finally`, or let it escape. That last point is the one interviewers push on: the generator is not merely resumed after an error, it is resumed *by having the error raised at the suspension point*. ## Exactly one yield, per call The helper enforces the single-yield contract with clear errors, all of which you can trigger in a REPL: ```python import contextlib @contextlib.contextmanager def never_yields(enabled): if not enabled: return # RuntimeError: generator didn't yield yield @contextlib.contextmanager def yields_twice(): yield 1 yield 2 # RuntimeError: generator didn't stop ``` Entering `never_yields(False)` raises `RuntimeError("generator didn't yield")` because `__enter__` got `StopIteration` instead of a value. Leaving `yields_twice()` raises `RuntimeError("generator didn't stop")` because the generator produced a second value where it should have finished. Both messages are worth recognising on sight — they almost always mean an early `return` on a conditional path or a stray second `yield`, not a bug in the standard library. ## Yielding nothing is normal A plain `yield` with no value yields `None`, which is exactly right for a manager that exists only for its side effects — a timer, a lock, a log-context switch. You then write `with timed():` and omit the `as`. Yielding a value is for managers that hand the block a resource, such as an open connection or a cursor. ## Where the state lives Because the generator's local variables stay alive across the suspension, the setup phase can simply keep what teardown needs in a local: ```python import contextlib, time @contextlib.contextmanager def timed(label): started = time.perf_counter() try: yield finally: print(label, round(time.perf_counter() - started, 3)) ``` With a class, `started` would have to be stored on `self` in `__enter__` and read back in `__exit__`. The generator form removes that bookkeeping entirely, which is most of why it is the default choice for short managers. ## What you give up The object the factory returns is **single-use**: its generator is consumed by the first `with`, so a second `with` on the same object fails. Call the factory again for each block. A generator-based manager is also not reentrant, cannot easily expose extra methods or attributes to the block, and cannot suppress an exception by returning a truthy flag — it suppresses by catching the exception at the `yield` and not re-raising. When you need reuse, reentrancy, inheritance, or a richer object, write the class instead; when you need setup/teardown around a block, the decorator is shorter and harder to get wrong. ## Practical shape The canonical body is three lines of skeleton: setup, `try: yield <value>`, `finally: teardown`. If a manager does not fit that shape — several yields, cleanup that depends on which exception occurred in a complicated way, or state the caller must inspect afterwards — that is the signal to reach for a class-based implementation rather than to bend the generator. ## Naming the thing you are decorating One wording trap is worth fixing early, because interviewers listen for it. `@contextlib.contextmanager` is applied to a generator *function*; what that function returns when called is a context-manager *object*, and inside it lives a generator *object*. "The context manager" in conversation should mean the object the `with` statement operates on, not the function you wrote. Getting those three names straight makes every later question — single use, exception delivery, decorator reuse — easy to state precisely instead of gesturing at "the generator".

  • What does the block receive if the generator writes a bare `yield` with no value?
    `None`, because a bare `yield` yields `None` and that becomes the `__enter__` return value. That is the normal shape for a manager that exists only for its side effects — a timer, a lock, a temporary directory change — and you simply write `with timed():` without an `as` clause.
  • When would you still write the class form with `__enter__` and `__exit__` instead?
    When the manager must be reusable or reentrant, when the object needs its own methods or attributes for the block to call, when it is part of a class hierarchy others subclass, or when cleanup logic is long enough that the three-argument `__exit__` signature reads more clearly than an `except`/`finally` chain around a `yield`.
  • Does calling the decorated function run any of the generator's body?
    No. Calling it only builds the generator object and wraps it in the context-manager helper; not a single statement of the body executes. The setup code runs when `with` calls `__enter__`, which is why a manager created but never entered performs no work and needs no cleanup.

The yield is a doorway you stand in: everything you did on the way to the door is entry, and everything after it is what you do once the visitor has left the room.

saying these in an interview costs you the question

  • Says the decorator works on any function, not a generator function
  • Thinks calling the decorated function runs the setup code
  • Believes multiple yields split the block into stages
  • Claims the yielded value is discarded rather than bound by as
  • Puts teardown after a bare yield and calls it equivalent to finally

context