skip to content

Context Managers and the with Statement

Python's deterministic cleanup mechanism: the with statement, the __enter__/__exit__ protocol, and the contextlib toolbox. Interviewers ask how you guarantee a file, lock or connection is released.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

23

How does contextlib.asynccontextmanager turn an async generator function into an async context manager?

level: juniorimportance: must knowfreq 45%

answer

  1. One function, two halves of a lifecycle
  2. The split point is a suspension
  3. Setup and teardown may both await
  4. Exactly one yield, wrapped in finally
  5. Calling it builds; async with runs it

basics

~20 s

contextlib.asynccontextmanager decorates an async generator function that yields exactly once. Calling it builds an object for use with async with: everything before the yield is the setup, the yielded value is what as binds, and everything after the yield is the teardown.

solid answer

~40 s

`contextlib.asynccontextmanager` wraps an `async def` function containing exactly one `yield`. Calling the decorated function runs none of the body — it returns a manager object that implements `__aenter__` and `__aexit__`. Entering it with `async with` advances the generator to the `yield`, awaiting whatever the setup awaits, and binds the yielded value to the `as` name. Leaving the block resumes the generator: on a clean exit it simply runs on, and on an exception the wrapper throws that exception in at the `yield` point. So teardown belongs in a `finally` around the `yield`, not merely after it. Yielding twice, or never reaching a `yield`, raises `RuntimeError`. The point of the async form is that both setup and teardown may `await`, which a synchronous `__enter__` cannot.

code

python · 18 lines
python
import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def sensor_session(name):
    print("open", name)
    await asyncio.sleep(0)
    try:
        yield {"name": name}
    finally:
        await asyncio.sleep(0)
        print("close", name)

async def main():
    async with sensor_session("rack-a") as session:
        print("reading", session["name"])

asyncio.run(main())

go deeper

for a junior

Recall the shape: one async def, one yield, teardown in a finally, used with async with. Be able to write a five-line example from memory and say which half runs on entry and which on exit.

for a middle

Explain the mechanics: calling the factory runs nothing, entering advances to the yield, exiting resumes it, and an exception in the block is thrown in at the yield. Know both RuntimeError contract violations.

for a senior

Show judgement about when the generator form is the wrong tool — re-entrant or reusable managers, managers that must expose attributes, or resources needing both the sync and async protocols — and why teardown outside a finally is a latent leak.

for a principal

Own the convention: which resources in a codebase get a decorated generator versus a class, so that async lifecycles are consistent and reviewable rather than each author inventing their own acquisition and release story.

### What the decorator actually produces `contextlib.asynccontextmanager` takes an **async generator function** — a function declared with `async def` whose body contains a `yield` — and returns a factory. Calling that factory executes none of the body. It constructs the async generator object and wraps it in a helper that implements the asynchronous context-manager protocol, `__aenter__` and `__aexit__`. The body starts running only when that wrapper is entered by an `async with` statement. ### Entering `__aenter__` drives the generator forward to its single `yield`, awaiting anything the setup code awaits along the way, and returns the yielded value. That value is what the `as` clause binds. This is the whole reason the async variant exists: setup can `await` a handshake, a slow first read, or an asynchronous lock acquisition before handing control to the block. A synchronous `__enter__` has no way to await anything, so a coroutine-based resource cannot be opened from one without blocking the event loop. ### Exiting `__aexit__` resumes the generator. If the block finished normally, the wrapper resumes it and expects it to run to completion. If the block raised, the wrapper **throws that exception into the generator at the point of the `yield`**, so the code around the `yield` observes the failure exactly as if the block had been written inline. This is why teardown must live in a `try: ... finally: ...` around the `yield` rather than simply after it: statements placed after a bare `yield` never execute when the body raises, and the resource is silently left open. Catching the thrown exception at the `yield` and not re-raising it is how the generator form suppresses an exception. ### Exactly one yield, every time The contract is one `yield` per use, on every path. If the generator yields a second time when teardown resumes it, the wrapper raises `RuntimeError` with the message *generator didn't stop*. If a guard clause returns before reaching any `yield`, `__aenter__` raises `RuntimeError` with *generator didn't yield*. Both are contract violations of the decorator, not failures of the underlying resource, and both are usually fixed by restructuring so the single `yield` is unconditional and the conditional logic sits before or after it. ### Single-use objects, reusable functions The decorated *function* is reusable; the object a single call returns is not, because an async generator can be driven to completion only once. Two nested blocks therefore need two calls to the factory. Since **3.10** the returned object can also be used as a decorator on an `async def` function, running the setup before the function body and the teardown after it — handy for cross-cutting instrumentation without an extra `async with` line inside every function: ```python import asyncio from contextlib import asynccontextmanager @asynccontextmanager async def trace(): print("enter") try: yield finally: print("exit") @trace() async def collect(): print("body") asyncio.run(collect()) ``` ### Why prefer it over a hand-written class A class with `__aenter__` and `__aexit__` splits one lifecycle across two methods and forces the shared state into instance attributes. The generator form keeps acquisition and release adjacent in one function, lets ordinary local variables carry the state between them, and reuses the language's own `try`/`finally` semantics for cleanup ordering instead of re-implementing them in `__aexit__`. For most resources — an opened session, a temporary subscription, a timing span, a paused background poller — that is smaller and much harder to get wrong. ### Where the class form still wins The generator form is single-use, so an object meant to be entered repeatedly, or re-entered while already active, needs a real class. Anything that must be inspected between entries — a manager exposing its own attributes or methods — also wants a class, since the decorator hands the block only whatever value was yielded. And a manager that must support both `with` and `async with` needs both method pairs, which one decorated generator cannot provide. ### One practical caution Because the object is produced by calling the factory, forgetting the call is a common error: `async with sensor_session:` on the undecorated *function* fails, while `async with sensor_session("rack-a"):` is correct. The failure is a protocol error rather than a subtle bug, which is fortunate — nothing has been opened yet at that point.

  • Where exactly does an exception raised inside the async with block appear in the decorated generator?
    At the `yield`. The wrapper throws it into the suspended generator, so the `yield` expression itself raises. Code guarded by a `try`/`finally` around the `yield` therefore runs on both the success and failure paths, while statements written after a bare `yield` are skipped entirely when the block raises. Catching that exception at the `yield` and returning without re-raising suppresses it.
  • Can you reuse the object returned by one call to a decorated function in two nested blocks?
    No. The object drives one async generator, and a generator can run to completion only once, so a second entry fails. The decorated *function* is reusable — call it again to get a fresh manager. If a genuinely re-enterable or reusable manager is needed, write a class with `__aenter__` and `__aexit__` instead.
  • What happens if the decorated generator returns without ever reaching its yield?
    Entering it raises `RuntimeError` — the wrapper needs a value from the `yield` to hand to the `as` clause and has nothing. It usually means a guard clause returns early. The fix is to keep exactly one unconditional `yield` and move the conditional logic before it, raising a real exception if setup genuinely cannot proceed.

The yield is the counter at a hire shop: everything before it hands you the kit, everything after it takes the kit back, and the finally is the clause that takes it back even when you return it damaged.

saying these in an interview costs you the question

  • Thinks calling the decorated function runs the setup code
  • Puts teardown after a bare yield instead of in finally
  • Believes several yields give several entries
  • Claims the setup cannot await anything
  • Reuses one returned manager object for nested blocks
  • Confuses the decorated function with the object it returns

context

open as a page

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

level: juniorimportance: must knowfreq 70%

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.

open as a page

Does a with statement catch exceptions raised in its body, or only run cleanup?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Only cleanup. The with statement calls the manager's exit on the way out, so the file gets closed or the lock released, but the exception keeps travelling up unless exit deliberately returns a truthy value.

open as a page

In Python, what does `with obj as x` bind to `x`, and which two methods must `obj` define?

level: juniorimportance: must knowfreq 78%

basics

~20 s

x is bound to whatever __enter__ returns, not to obj itself. Any object usable in a with statement must define both __enter__ and __exit__; __exit__ runs on the way out whether the block succeeded or raised.

open as a page

How does contextlib.suppress(FileNotFoundError) differ from try/except/pass?

level: middleimportance: must knowfreq 50%

basics

~10 s

They do the same thing: contextlib.suppress(FileNotFoundError) is the with-statement spelling of try/except FileNotFoundError: pass. It reads as deliberate rather than accidental, and since Python 3.12 it also strips matching exceptions out of an ExceptionGroup.

open as a page

Why must a @contextlib.contextmanager generator wrap its yield in try/finally?

level: middleimportance: must knowfreq 60%

basics

~20 s

When the with body raises, the exception is re-raised inside the generator at the yield expression. Any statement written after a bare yield is then skipped, so cleanup only runs on the error path if the yield sits inside try/finally.

open as a page

Why use contextlib.ExitStack instead of a with statement listing fixed managers?

level: middleimportance: must knowfreq 45%

basics

~20 s

Because a with statement has to name its managers when the code is written. ExitStack lets you enter a number of resources decided at runtime - a loop over a list - while keeping the same guaranteed, reverse-order cleanup.

open as a page

What does returning True from a context manager's __exit__ do to the exception?

level: middleimportance: must knowfreq 62%

basics

~10 s

A truthy return from exit swallows the exception: it is discarded and execution resumes on the line after the with block, not where the error was raised. Returning None or False lets it propagate.

open as a page

What three arguments does a context manager's `__exit__` receive when a `with` block ends?

level: middleimportance: must knowfreq 64%

basics

~20 s

__exit__(self, exc_type, exc_value, tb) receives the exception class, the exception instance and the traceback object. When the block finishes without raising, all three are None — that triple is how the method tells success from failure.

open as a page

What does contextlib.closing() do for an object that has close() but no __enter__?

level: juniorimportance: should knowfreq 35%

basics

~10 s

contextlib.closing(obj) wraps any object in a context manager: enter returns obj unchanged and exit calls obj.close(). It lets a close()-only object be used in a with statement instead of a hand-written try/finally.

open as a page

In what order does contextlib.ExitStack unwind the cleanups it registered?

level: juniorimportance: should knowfreq 30%

basics

~10 s

In reverse order of registration: last registered, first run. It is a stack, so it unwinds exactly as equivalent nested with statements would, closing the most recently entered resource first.

open as a page

Why does contextlib.ExitStack.enter_context reject an async context manager, and what replaces it?

level: middleimportance: should knowfreq 34%

basics

~10 s

contextlib.ExitStack drives the synchronous protocol only, so passing it an object that defines aenter and aexit raises TypeError. Use contextlib.AsyncExitStack instead, entering resources with await stack.enter_async_context(cm).

open as a page

How does contextlib.nullcontext keep an optional lock from forcing two copies of a with block?

level: middleimportance: should knowfreq 30%

basics

~20 s

contextlib.nullcontext() is a context manager that does nothing: its enter returns its argument (None by default) and its exit ignores everything. Choose it or the real manager into one variable, then write the with block once.

open as a page

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

level: middleimportance: should knowfreq 35%

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.

open as a page

How do ExitStack.callback and push differ from enter_context on the same stack?

level: middleimportance: should knowfreq 32%

basics

~10 s

enter_context calls a manager's enter and registers its exit. push registers an exit-style callable without entering anything. callback registers a plain function plus arguments, called at unwind time and never shown exception details.

open as a page

If a context manager's __enter__ raises, is its __exit__ still called?

level: middleimportance: should knowfreq 30%

basics

~20 s

No. The with statement only commits to calling exit once enter has returned successfully, so a manager that fails partway through acquisition must clean up after itself inside enter or leak whatever it already took.

open as a page

How does `with A() as a, B() as b:` differ from two nested `with` statements?

level: middleimportance: should knowfreq 46%

basics

~20 s

It does not differ semantically — the comma form is defined as exactly equivalent to nesting. Managers are entered left to right and exited in reverse, and each one's __exit__ still runs if a later manager fails.

open as a page

Why wrap an async generator in contextlib.aclosing when you break out of an async for early?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Breaking out of an async for leaves the async generator suspended at its yield, so its finally block does not run until the generator is finalized later. contextlib.aclosing awaits aclose on block exit, running that cleanup deterministically at a point you choose.

open as a page

Why can contextlib.redirect_stdout fail to capture the output you expected?

level: seniorimportance: should knowfreq 28%

basics

~20 s

It only rebinds the name sys.stdout, process-wide, for the block's duration. Writes through a saved stream reference, straight to descriptor 1 from C code, or from a child process escape it; other threads' prints get captured.

open as a page

A @contextlib.contextmanager caught the with body's error and never re-raised it — what happens?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The exception is suppressed. Execution continues on the line after the with block as if the body had succeeded, which is how a generator-based manager swallows errors — the equivalent of a class-based manager returning True from its exit method.

open as a page

How does contextlib.ExitStack.pop_all() let a factory return resources it opened?

level: seniorimportance: should knowfreq 28%

basics

~20 s

pop_all() moves every registered cleanup onto a new ExitStack and empties the original, so the original's exit becomes a no-op. Setup failures still roll back; once setup succeeds, the caller receives the new stack and owns the teardown.

open as a page

What happens if __exit__ itself raises while the with body is already unwinding an exception?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The cleanup exception wins: it propagates and replaces the original, which survives only as the new exception's context and prints under "During handling of the above exception, another exception occurred". Callers matching the original type stop matching.

open as a page

Why does re-entering the same context-manager object nested break it, and what makes one reentrant?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

A manager that keeps per-entry state in a single instance attribute clobbers it on the second entry, so the first __exit__ restores the wrong value. Reentrancy requires per-entry state — a stack of saved values, or a fresh token returned by each __enter__.

open as a page