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.

Overview

The question, and what it is testing

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.

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

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."

What does `with` guarantee when the body raises?

What does the with statement actually do, and what happens to an exception raised inside it?

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 necessarily 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 exception. 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 exception has been handled, and execution continues 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 usually a mistake, because a manager that swallows exceptions silently is a manager that hides bugs. The convention is to return None and let the exception through, doing cleanup on the way out. If you do suppress, suppress one specific 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)

Everything before the yield is __enter__; everything after is __exit__. The try/finally is not optional — without it, an exception in the body propagates out of the yield and the cleanup never runs, which 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 difference is that the cleanup lives with the resource rather than with every 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__, which is what an async HTTP client or database session uses — same protocol, awaitable methods.

What to say 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.

Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.

What to notice while it runs

  • __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.

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 - which 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, every exception in every body disappears - including KeyboardInterrupt and typos. Suppress one named type or nothing.

The follow-ups interviewers ask

"What if __enter__ itself raises?" Then __exit__ is not called, because the block was never entered. This catches people: any resource acquired partway through __enter__ has to be cleaned up inside __enter__ itself. It is also why ExitStack exists for multi-resource setup.

"How do you manage a variable number of resources?" contextlib.ExitStack: enter each one into the stack and it unwinds them all in reverse on exit, however many there are. The alternative - a recursive or dynamically nested with - is not writable.

"What is the async equivalent?" async with, backed by __aenter__ and __aexit__, which are coroutines. Same protocol, awaitable methods, and it is what an async HTTP client or database session gives you.

Common wrong answers

"with catches exceptions." It does not. It guarantees the cleanup and lets the exception through, unless __exit__ returns something truthy - which is unusual and should be deliberate.

"It is just try/finally." Same effect, worse locality: the cleanup lives at every call site instead of with the resource. That is the argument, and the interviewer is usually listening for it.

"__exit__ receives the block's return value." It receives the exception type, value and traceback - or three Nones. It never sees the body's result.

Recap in one screen

  • __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.
  • The one-line answer: with calls __enter__, runs the body, and calls __exit__ however the body ends - normally, by return, or by exception.
  • Worth trying: 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.
  • Worth trying: Replace case 3 with contextlib.suppress(ValueError) and confirm it behaves the same. That is all suppress is.

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?

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.

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.