Skip to content

Context managers

Senior

The 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

  1. 01What a context manager is — the `__enter__` / `__exit__` protocol behind `with`

    A 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. The with statement 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() raised
    

    open("file.txt") returns a context manager (a file object implementing __enter__/ __exit__). __enter__ runs first and its return value is bound to f; whatever happens inside the block, __exit__ runs afterward and closes the file. This is the same guarantee a try/finally gives 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.

    #c-what-a-context-manager-is-the-__enter__-__exit__-protocol-be
  2. 02The exact call sequence of `with` when the body raises — and what suppresses the exception

    Take this with statement 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:

    1. cm = SomeCM() — the context manager object itself is obtained, however that happens to occur.
    2. cm.__enter__() is called, and whatever it returns is bound to the name x (here, the string "resource").
    3. The body (print("got", x), then raise ValueError("boom")) executes and, in this scenario, raises partway through.
    4. cm.__exit__(exc_type, exc_value, traceback) is then called, receiving the actual exception’s type (ValueError), value, and traceback as its three arguments — not None, None, None, since a real exception genuinely occurred.
    5. The critical, final decision is __exit__'s return value: if it’s truthy, the exception is fully suppressed — execution simply continues on immediately after the with block, exactly as though nothing had gone wrong at all. If instead it returns anything falsy (including the implicit None Python provides by default whenever a function has no explicit return statement at all — the case above, since it returns False), the original ValueError re-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 the ValueError propagates out of the with block. 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 smell

    Suppression is occasionally legitimate — a context manager explicitly designed to say “this specific class of error is expected here and I’m handling it” (contextlib.suppress is exactly that, and it checks issubclass(exc_type, self._exceptions) before returning True). 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 every with block 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 return an arbitrary expression from __exit__; either fall off the end (returns None) or return an explicit boolean that narrowly names the exceptions you mean to suppress.

    #c-the-exact-call-sequence-of-with-when-the-body-raises-and-wha
  3. 03What `__exit__` receives with no exception, and why `__enter__` raising skips `__exit__`

    In the no-exception case, __exit__ is still called with all three positional arguments, exactly as always — just with all three set to None (__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 — the with statement 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 internal try/except), since __exit__ simply will never be reached to handle it for you in this particular failure case.

    #c-what-__exit__-receives-with-no-exception-and-why-__enter__-r
  4. 04Multiple context managers in one `with` — enter/exit order vs nested blocks

    This combined single-statement form is genuinely functionally equivalent to properly nesting two separate with blocks: a is entered first, then b second — and, critically, on the way out, they unwind in strictly reverse order (b.__exit__ runs before a.__exit__ does), exactly matching what properly nested with blocks 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., if b’s own resource setup genuinely depends on a’s resource already being open, then b correctly needs to be closed and released before a is, and this reverse-unwind behavior is precisely what guarantees that happens correctly.

    #c-multiple-context-managers-in-one-with-enterexit-order-vs-nes
  5. 05`contextlib.ExitStack` — managing a runtime-variable number of context managers

    ExitStack specifically 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, fixed with 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 — and ExitStack still 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.txt
    

    This 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 of open(...) calls isn’t fixed at code-writing time, it’s driven by whatever filenames turns out to contain at runtime. If open() raises partway through the loop (say, b.txt doesn’t exist), ExitStack still closes everything that was successfully registered before that point (just a.txt here) as the with block unwinds — nothing opened earlier leaks.

    #c-contextlibexitstack-managing-a-runtime-variable-number-of-co
  6. 06Why a `@contextmanager` generator also works as a decorator — `ContextDecorator`
    from 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 to handle_request produce 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.contextmanager internally wraps your yield-based generator function inside a class that implements both the ordinary context-manager protocol and — since that class also inherits from ContextDecorator — __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 a with block’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 separate with block cluttering up the function’s own body.

    #c-why-a-contextmanager-generator-also-works-as-a-decorator-con
  7. 07Where an exception lands in a `@contextmanager` generator — why the `yield` needs `try`/`finally`

    The exception gets thrown directly into the generator function at the exact point of the yield statement itself, via the same .throw() mechanism used for delivering any exception into a paused generator frame — meaning any code positioned before the yield (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 the with block’s body raised or not, that cleanup absolutely must live inside a finally (or a suitably matching except) block wrapped directly around the yield itself — code simply placed after a bare, unwrapped yield, with no surrounding try/finally at 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.

    #c-where-an-exception-lands-in-a-contextmanager-generator-why-t
  8. 08Class-based vs `contextlib.contextmanager` context managers — the exception-handling tradeoff

    A context manager is any object implementing __enter__ and __exit__, used with the with statement to guarantee setup/teardown logic runs correctly, including on error. The class-based version defines both methods explicitly on a class. The contextlib.contextmanager decorator offers a much more concise alternative: you write a single generator function with exactly one yield in the middle — everything before the yield becomes the equivalent of __enter__ (the setup), the yielded value becomes what’s bound by as, and everything after the yield becomes 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 with block’s body, the generator form receives it thrown into the generator at the exact point of the yield (via the generator’s .throw() method) — so if you don’t wrap the yield in try/except/finally, any cleanup code after the yield simply never executes on the error path. Suppression is implicit too: catching the exception around the yield and not re-raising it suppresses it, which is easy to do by accident with a broad except. 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 in try/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 active with block. A generator-based version can only hand out whatever it yields; 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 @contextmanager object is single-use, because its underlying generator can only run once).

    #c-class-based-vs-contextlibcontextmanager-context-managers-the