Context managers
SeniorThe full with protocol — call sequencing, what __exit__ returns, ExitStack, and the generator-based form's traps.
Created 2 September 2026 · Updated 23 October 2026
8 concepts · detail hidden
01What a context manager is — the `__enter__` / `__exit__` protocol behind `with`
#c-what-a-context-manager-is-the-__enter__-__exit__-protocol-beA context manager is any object that implements two specific methods,
__enter__and__exit__. That pair of methods is the entire protocol — nothing more is required. Thewithstatement is the syntax that drives this protocol: it’s how Python guarantees some setup code runs before a block, and some cleanup code runs after it, even if the block raises an exception.with open("file.txt") as f: data = f.read() # f.close() has already run here, even if .read() raisedopen("file.txt")returns a context manager (a file object implementing__enter__/__exit__).__enter__runs first and its return value is bound tof; whatever happens inside the block,__exit__runs afterward and closes the file. This is the same guarantee atry/finallygives you, just packaged so the setup/cleanup pairing lives on the object instead of being rewritten at every call site:f = open("file.txt") try: data = f.read() finally: f.close()The next few concepts below walk through that protocol’s exact mechanics — call order, arguments, and what makes
__exit__suppress an exception instead of letting it propagate.02The exact call sequence of `with` when the body raises — and what suppresses the exception
#c-the-exact-call-sequence-of-with-when-the-body-raises-and-whaTake this
withstatement as the running example for the whole sequence below:class SomeCM: def __enter__(self): print("enter") return "resource" def __exit__(self, exc_type, exc_value, traceback): print("exit", exc_type) return False # True would suppress the exception instead with SomeCM() as x: print("got", x) raise ValueError("boom")The precise, complete sequence this code triggers:
cm = SomeCM()— the context manager object itself is obtained, however that happens to occur.cm.__enter__()is called, and whatever it returns is bound to the namex(here, the string"resource").- The body (
print("got", x), thenraise ValueError("boom")) executes and, in this scenario, raises partway through. cm.__exit__(exc_type, exc_value, traceback)is then called, receiving the actual exception’s type (ValueError), value, and traceback as its three arguments — notNone, None, None, since a real exception genuinely occurred.- The critical, final decision is
__exit__'s return value: if it’s truthy, the exception is fully suppressed — execution simply continues on immediately after thewithblock, exactly as though nothing had gone wrong at all. If instead it returns anything falsy (including the implicitNonePython provides by default whenever a function has no explicitreturnstatement at all — the case above, since it returnsFalse), the originalValueErrorre-raises normally, continuing to propagate upward, once__exit__itself has finished running.
So the example above prints
enter,got resource,exit <class 'ValueError'>, and then theValueErrorpropagates out of thewithblock. The precise “falsy re-raises, truthy suppresses” rule is the part of this protocol most often misremembered.Why a truthy
__exit__return is usually a design smellSuppression is occasionally legitimate — a context manager explicitly designed to say “this specific class of error is expected here and I’m handling it” (
contextlib.suppressis exactly that, and it checksissubclass(exc_type, self._exceptions)before returningTrue). But it’s a common accidental bug source: if__exit__ends by returning the result of some cleanup call that happens to be truthy, it silently swallows every exception raised in everywithblock that uses the manager.class Session: def __exit__(self, exc_type, exc, tb): return self.conn.close() # BUG: if close() returns True, every exception vanishes class Session: def __exit__(self, exc_type, exc, tb): self.conn.close() # implicit None -> exceptions propagate # or, if suppression is intended, make it narrow and explicit: # return exc_type is not None and issubclass(exc_type, TimeoutError)Rule of thumb: never
returnan arbitrary expression from__exit__; either fall off the end (returnsNone) or return an explicit boolean that narrowly names the exceptions you mean to suppress.03What `__exit__` receives with no exception, and why `__enter__` raising skips `__exit__`
#c-what-__exit__-receives-with-no-exception-and-why-__enter__-rIn the no-exception case,
__exit__is still called with all three positional arguments, exactly as always — just with all three set toNone(__exit__(None, None, None)), rather than being called with a different, shorter signature or skipped altogether. If__enter__itself raises an exception, though,__exit__is genuinely never called at all — thewithstatement never successfully, fully “entered” in the first place, so there’s structurally nothing meaningful to “exit” from at all. This is a real, practical gotcha for anyone whose__enter__performs partial, multi-step setup that itself might need cleanup on its own internal failure — that specific cleanup logic genuinely has to live inside__enter__itself (typically via its own internaltry/except), since__exit__simply will never be reached to handle it for you in this particular failure case.04Multiple context managers in one `with` — enter/exit order vs nested blocks
#c-multiple-context-managers-in-one-with-enterexit-order-vs-nesThis combined single-statement form is genuinely functionally equivalent to properly nesting two separate
withblocks:ais entered first, thenbsecond — and, critically, on the way out, they unwind in strictly reverse order (b.__exit__runs beforea.__exit__does), exactly matching what properly nestedwithblocks would naturally produce, rather than simply running in the same order they were originally listed for entry. This ordering genuinely matters for real resource-dependency scenarios — e.g., ifb’s own resource setup genuinely depends ona’s resource already being open, thenbcorrectly needs to be closed and released beforeais, and this reverse-unwind behavior is precisely what guarantees that happens correctly.05`contextlib.ExitStack` — managing a runtime-variable number of context managers
#c-contextlibexitstack-managing-a-runtime-variable-number-of-coExitStackspecifically handles managing a variable, only-known-at-runtime number of context managers — for instance, opening some genuinely unknown-in-advance number of files, based on a list of filenames only computed dynamically at runtime — a situation a static, fixedwith a, b, c:statement fundamentally cannot express at all, since that syntax structurally requires every individual context manager to be explicitly, statically known and written out at the point you’re actually writing the code.stack.enter_context(cm), called inside a loop as many times as needed, dynamically registers each context manager one at a time — andExitStackstill guarantees they all correctly, properly unwind (in reverse registration order, the same reverse-unwind rule that applies to any nested context managers) once the enclosing stack itself exits, regardless of how many were actually registered along the way, even when that count genuinely wasn’t known until the loop actually ran.from contextlib import ExitStack filenames = ["a.txt", "b.txt", "c.txt"] # only known at runtime with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # all three files are open here data = [f.read() for f in files] # on the way out, they close in reverse order: c.txt, then b.txt, then a.txtThis is equivalent to writing
with open("a.txt") as fa, open("b.txt") as fb, open("c.txt") as fc:by hand — except the number ofopen(...)calls isn’t fixed at code-writing time, it’s driven by whateverfilenamesturns out to contain at runtime. Ifopen()raises partway through the loop (say,b.txtdoesn’t exist),ExitStackstill closes everything that was successfully registered before that point (justa.txthere) as thewithblock unwinds — nothing opened earlier leaks.06Why a `@contextmanager` generator also works as a decorator — `ContextDecorator`
#c-why-a-contextmanager-generator-also-works-as-a-decorator-confrom contextlib import contextmanager @contextmanager def timed(label): start = time.time() try: yield finally: print(f"{label}: {time.time() - start:.3f}s") # used as a `with` block with timed("query"): run_query() # the exact same function, used as a decorator instead @timed("handler") def handle_request(): run_query()with timed("query"): run_query()and@timed("handler")applied tohandle_requestproduce the same setup/teardown behavior around the wrapped code — the only difference is whether you’re wrapping one inline block or every call to a named function.@contextlib.contextmanagerinternally wraps youryield-based generator function inside a class that implements both the ordinary context-manager protocol and — since that class also inherits fromContextDecorator—__call__-based decoration support simultaneously, entirely automatically. Used as a decorator, the entire body of the decorated function effectively plays the exact same role that awithblock’s own body would ordinarily play — the same setup/teardown logic runs around each individual call to the decorated function itself. The observable, actual runtime behavior is essentially identical between the two usage styles — genuinely just a syntactic-style choice, not a semantic one. The decorator form is the natural choice specifically when you want that same setup/teardown logic applied consistently around an entire function’s execution, every single time it’s called, without needing a visibly separatewithblock cluttering up the function’s own body.07Where an exception lands in a `@contextmanager` generator — why the `yield` needs `try`/`finally`
#c-where-an-exception-lands-in-a-contextmanager-generator-why-tThe exception gets thrown directly into the generator function at the exact point of the
yieldstatement itself, via the same.throw()mechanism used for delivering any exception into a paused generator frame — meaning any code positioned before theyield(your genuine setup logic) has already run normally by this point, entirely unaffected; but if you actually want cleanup logic to run reliably regardless of whether thewithblock’s body raised or not, that cleanup absolutely must live inside afinally(or a suitably matchingexcept) block wrapped directly around theyielditself — code simply placed after a bare, unwrappedyield, with no surroundingtry/finallyat all, genuinely never executes at all if the body happens to raise, since the thrown exception propagates straight out of the generator function immediately at that exact point, rather than ever falling through naturally to any subsequent lines below it.08Class-based vs `contextlib.contextmanager` context managers — the exception-handling tradeoff
#c-class-based-vs-contextlibcontextmanager-context-managers-theA context manager is any object implementing
__enter__and__exit__, used with thewithstatement to guarantee setup/teardown logic runs correctly, including on error. The class-based version defines both methods explicitly on a class. Thecontextlib.contextmanagerdecorator offers a much more concise alternative: you write a single generator function with exactly oneyieldin the middle — everything before theyieldbecomes the equivalent of__enter__(the setup), theyielded value becomes what’s bound byas, and everything after theyieldbecomes the equivalent of__exit__(the teardown).from contextlib import contextmanager class Transaction: def __init__(self, conn): self.conn = conn self.finished = False def __enter__(self): self.conn.execute("BEGIN") return self # the caller gets the manager itself def rollback(self): # extra method, callable mid-block self.conn.execute("ROLLBACK") self.finished = True def __exit__(self, exc_type, exc, tb): if not self.finished: self.conn.execute("ROLLBACK" if exc_type else "COMMIT") # falls off the end -> None -> exceptions propagate @contextmanager def transaction(conn): conn.execute("BEGIN") try: yield conn # body exceptions are thrown in HERE except BaseException: conn.execute("ROLLBACK") raise # forget this and the exception is suppressed else: conn.execute("COMMIT")The exception-handling tradeoff
If an exception occurs inside the
withblock’s body, the generator form receives it thrown into the generator at the exact point of theyield(via the generator’s.throw()method) — so if you don’t wrap theyieldintry/except/finally, any cleanup code after theyieldsimply never executes on the error path. Suppression is implicit too: catching the exception around theyieldand not re-raising it suppresses it, which is easy to do by accident with a broadexcept. The class-based approach doesn’t have this trap in the same shape —__exit__is always called with the exception info as explicit arguments, so there’s no “did I remember to wrap it intry/finally” mistake, though__exit__still has to decide correctly whether to suppress (truthy return) or re-raise.When the class form is the better choice
For simple setup/teardown the two are functionally equivalent and the choice is largely syntax preference. The class form becomes the objectively better choice when the context manager needs to expose additional state or methods beyond entering and exiting — like
Transaction.rollback()above, which calling code can invoke from within the activewithblock. A generator-based version can only hand out whatever ityields; it has no clean way to expose further callable behaviour of the manager itself mid-block. The class form is also preferable when the manager must be reusable (a@contextmanagerobject is single-use, because its underlying generator can only run once).