What does `with` guarantee when the body raises?

with calls __enter__, runs the body, and calls __exit__ however the body ends — normally, by return, or by exception. __exit__ is told what went wrong, and if it returns something truthy the exception is suppressed.

Python semanticsConceptualMedium

Step through it

What to watch

  • __exit__ runs in all three cases — that is the guarantee being bought.
  • It is told the exception type, so cleanup can behave differently on failure.
  • Returning True swallows the exception; returning None lets it propagate.

Say this out loud

"with is the context manager protocol: __enter__ before the body, __exit__ after it, guaranteed, including on an exception or a return. __exit__ receives the exception details, and returning True from it suppresses the exception - which is exactly what contextlib.suppress does. I use it for anything with a cleanup that must happen."

Overview

A with statement runs a context manager’s entry and exit methods around a block. The exit method runs even when the block raises, allowing resources to be released and, when designed to, exceptions suppressed.

Run it

All three exits observed from inside __exit__, the suppression case. And a return from the body that still runs the cleanup.

All three exits, observed from inside __exit__.

1Python
Output

A return from inside the body cannot skip the cleanup either.

2Python
Output

The generator form, and why the try/finally is not optional.

3Python
Output

The protocol, in full

with expr as name: does four things. It evaluates expr. It calls __enter__() on the result and binds the return value to name. Note that this is not always the object itself, which is why open() can return a file and a lock can return None. It runs the body. Then it calls __exit__(exc_type, exc, traceback).

The last step is the point. It happens on a normal fall-through, on a return from inside the body, on a break. And on an error. That is a stronger promise than try/finally written by hand, only because it is harder to forget.

Suppression, and why it is rare

A truthy return from __exit__ tells Python the error has been handled, and execution goes on after the block. It is a real feature with one common use — contextlib.suppress(FileNotFoundError) is a context manager whose entire job is to return True for the types you named.

Writing it yourself is often a mistake because a manager that swallows errors silently is a manager that hides bugs. The convention is to return None and let the error through, doing cleanup on the way out. If you do suppress, suppress one set type and nothing else.

contextlib, and the generator form

Writing a class with two dunder methods for a two-line cleanup is heavy, and contextlib.contextmanager removes it:

@contextmanager
def timed(label):
    start = time.perf_counter()
    try:
        yield                  # the body runs here
    finally:
        print(label, time.perf_counter() - start)

All of it before the yield is __enter__. All of it after is __exit__. The try/finally is not optional — without it, an error in the body propagates out of the yield and the cleanup never runs. This quietly defeats the entire purpose.

ExitStack is the other one worth knowing. It manages a variable number of managers. So you can enter a list of files without a nested with per file.

The follow-up: why not just use try/finally

You can, and with compiles to roughly that. The gap is that the cleanup lives with the resource rather than with each call site. So it cannot be forgotten at the twelfth place the resource is used. It also composes: with a, b: nests correctly. And both exits run even if the first one raises.

The async counterpart is async with and __aenter__/__aexit__. This is what an async HTTP client or store session uses. Same protocol, awaitable methods.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

with a, b: is not the same as two separate blocks on failure. It nests, so if b's __enter__ raises, a's __exit__ still runs -. This is the behaviour you want and the reason to prefer the comma form over two statements.

A @contextmanager generator can only be used once. The object it returns is a one-shot generator. So re-entering the same manager instance raises. Call the factory again instead, or write a class.

Suppressing broadly hides bugs. If __exit__ returns True unconditionally, each error in each body disappears - including KeyboardInterrupt and typos. Suppress one named type or nothing.

Sources and next steps

How the code works

All three exits observed from inside __exit__, the suppression case, and a return from the body that still runs the cleanup.

How the code works

  1. return selfWhat __enter__ returns is what as binds. Returning self is a convention, not a rule — open() returns a file, and a lock returns None.
  2. return self.swallowThe return value of __exit__ is the suppression decision. Falsy lets the exception through; truthy stops it dead. Returning nothing means None, which is falsy, which is the right default.
  3. with Tracked("D"): return "returned"The exit prints before f() hands its value back. A return inside the block cannot skip the cleanup.
  4. try: yield ... finally:In the generator form the finally is what makes the cleanup unconditional. Omit it and an exception in the body escapes through the yield, leaving the cleanup unrun.

Change one thing

  • Delete the try/finally from guarded and re-run case 5. The exit line disappears — the bug that makes a hand-written context manager worse than none.
  • Replace case 3 with contextlib.suppress(ValueError) and confirm it behaves the same. That is all suppress is.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. When is __exit__ called?

  2. What does returning True from __exit__ do?

  3. What does `as name` bind?

  4. In the @contextmanager form, why is try/finally around the yield required?

Cheat sheet

What does `with` guarantee when the body raises?

Trace a with block that raises an exception. See the __enter__ and __exit__ calls, check whether the error propagates, and know what is guaranteed.

INTERVIEW · vizlearn.in/interview/what-with-guarantees.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.