skip to content

Why is a @contextlib.contextmanager object single-use once its with block has finished?

level: middleimportance: should knowfreq 35%

answer

  1. One call, one generator
  2. Bind the factory, not the object
  3. Exhausted generators do not restart
  4. The decorator rebuilds it per call
  5. Reusable means write the class

basics

~20 s

Each call of the decorated factory builds one generator, and the first with statement consumes it. Entering the same object a second time fails; call the factory again for every block that needs the manager.

solid answer

~40 s

The object the decorated function returns owns exactly one generator, and one `with` runs that generator to completion. A second `with` on the *same object* has no live generator left to advance, so CPython raises — on 3.14 an `AttributeError`, because `__enter__` also drops the arguments it would need to rebuild itself. The exact exception is an implementation detail; the contract is the point: **it is single-use, not reusable and not reentrant.** Store the *factory*, not the manager object, and call it per block. The one place reuse appears to work is the decorator form — `@my_manager()` above a function — and that is because the object inherits `contextlib.ContextDecorator`, whose wrapper recreates a fresh manager on every call of the decorated function.

code

python · 25 lines
python
import contextlib

@contextlib.contextmanager
def phase(label):
    print("enter", label)
    try:
        yield
    finally:
        print("exit", label)

cm = phase("once")
with cm:
    pass
try:
    with cm:
        pass
except Exception as exc:
    print("reuse failed:", type(exc).__name__)

@phase("charge")
def charge_customer():
    print("working")

charge_customer()
charge_customer()

go deeper

for a junior

Remember to write with make_manager(): — call the factory in the with line. Saving the result in a variable and using it in two with statements is the mistake this question is about.

for a middle

Explain why: each factory call owns one generator and one with exhausts it, so re-entry has nothing to advance. Know that the decorator form works because a fresh manager is built per call.

for a senior

Be ready to diagnose it: an error raised inside contextlib on a second execution of a code path, traced back to a shared manager object in a helper or test fixture, fixed by making the helper a callable.

for a principal

Frame the choice by lifetime — entered once per acquisition means a generator manager; one long-lived object entered from many places means a class, with reentrancy designed in rather than assumed.

## One call, one generator `@contextlib.contextmanager` turns a generator function into a factory. Each call to the factory creates a new generator object and a new wrapper around it. A `with` statement drives that generator from its start, through its `yield`, to its end — and a generator, once exhausted, cannot be restarted. So this is a bug: ```python import contextlib @contextlib.contextmanager def phase(label): print("enter", label) try: yield finally: print("exit", label) cm = phase("once") with cm: pass with cm: # boom pass ``` On CPython 3.14 the second `with` raises `AttributeError: '_GeneratorContextManager' object has no attribute 'args'`, because entering deliberately discards the stored factory arguments (they are only needed for recreation, which is no longer possible once the generator has started). Do not memorise that message — memorise the contract. The right answer in an interview is "the object is single-use; call the factory again", and only then, if pressed, "the concrete error is an implementation detail of CPython". ## Single-use, non-reusable, non-reentrant Three distinct properties travel together here, and it is worth separating them. * **Single-use** — the object can be entered once. That is what the example above violates. * **Non-reusable** — you cannot keep a module-level manager object around and use it from several places. `threading.Lock` is reusable; a generator-based manager is not. * **Non-reentrant** — you cannot nest the *same object* inside itself. Even a class-based manager is usually not reentrant unless it was designed to be; the generator form never is. The practical rule that covers all three: **bind the factory to a name, never the manager object.** `with phase("x"):` at each site is correct; `CM = phase("x")` at module level and `with CM:` in three functions is a latent failure that shows up the second time the code path runs — often only under a second request, a second row, or a retry. ## The trap in shared setup The common way to hit this is factoring out "the manager we always use" into a shared object rather than a shared function. Test helpers are the classic host: a module-level `TXN = transaction()` used by several tests works for whichever test runs first and fails for the rest, with an error that points at the standard library rather than at the helper. The fix is one character of indirection — make the helper a callable and call it per use. ## Where reuse genuinely works: the decorator form The object produced by `@contextlib.contextmanager` inherits `contextlib.ContextDecorator`, which means it can be used to decorate a function: ```python import contextlib @contextlib.contextmanager def phase(label): print("enter", label) try: yield finally: print("exit", label) @phase("charge") def charge_customer(): print("working") charge_customer() charge_customer() # works, twice ``` Both calls print their enter/exit pair. That is not reuse of one manager: `ContextDecorator`'s wrapper builds a **fresh** manager for each call of the decorated function, using the arguments the factory was originally given. This is exactly why `__enter__` may discard those arguments — once you have entered the object directly, recreation is off the table, so the decorator path only works from an object that was never entered by hand. The decorator form is the right tool when every call of a function needs the same setup and teardown and the body does not need the yielded value: timing, log context, a feature flag, a temporary directory. If the body needs what the manager yields, keep the explicit `with` inside the function, because the decorator has nowhere to bind an `as` target. ## Choosing the class form instead If you genuinely need one long-lived object that many blocks enter — a connection wrapper, a reusable lock-like guard — write the class with `__enter__` and `__exit__`, where each entry starts from the object's own state rather than from a consumed generator. Reentrancy still has to be designed in (a depth counter, or delegating to a reentrant primitive); a class does not grant it for free. The decision is therefore not "class versus decorator" in the abstract but "how many times will this object be entered": once per acquisition, generator; many times from one object, class. ## Diagnosing it in the wild The signature is an error inside `contextlib` on a `with` line that is obviously fine, appearing on the *second* execution of a path, never the first. Trace the manager expression backwards: if the `with` names a variable rather than a call, you have found it.

  • How can `@my_manager()` above a function work on every call if the manager is single-use?
    Because the object inherits `contextlib.ContextDecorator`. Its `__call__` returns a wrapper that, on each invocation, builds a fresh manager from the factory and its original arguments and runs the function inside a `with`. Each call therefore gets its own generator; the object you decorated with is never itself entered.
  • When is the decorator form the wrong choice compared with an explicit `with` inside the function?
    When the body needs the value the manager yields. A decorator has nowhere to bind an `as` target, so anything the manager hands out is unreachable. It also hides the scope: if only part of the function needs the setup, an inner `with` states that precisely, while the decorator wraps the whole call including argument-independent work.
  • What is the practical rule that prevents accidental reuse?
    Bind the factory to a name, never the manager object. `with make_txn():` at each call site is correct; a module-level `TXN = make_txn()` shared by several functions or tests fails the second time a path runs. If a single long-lived object really must be entered many times, that is the signal to write a class-based manager instead.

saying these in an interview costs you the question

  • Says the manager object can be reused like a threading.Lock
  • Stores a module-level manager object and enters it from several places
  • Thinks the generator restarts on the next with statement
  • Claims the decorator form reuses the same generator each call
  • Confuses single-use with not thread-safe

context