What three arguments does a context manager's `__exit__` receive when a `with` block ends?
answer
- Three parameters after `self`, always
- Class, instance, traceback
- The same triple `sys.exc_info()` reports
- All `None` means the block succeeded
- It runs like a `finally`, not an `except`
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.
solid answer
~40 sThe signature is fixed at exactly three parameters after `self`: `exc_type` (the exception **class**), `exc_value` (the exception **instance**), and `tb` (a traceback object). They are the same triple `sys.exc_info()` reports, and on any non-exceptional exit — falling off the end, `return`, `break`, `continue` — all three are `None`. So the standard test inside a teardown is `if exc_type is None: commit() else: rollback()`. `__exit__` runs on every exit path, which is what makes `with` a `try/finally` rather than a `try/except`; a manager that only wants cleanup can ignore all three parameters, and `*args` is a legal but sloppy way to accept them. Returning nothing (`None`) lets any in-flight exception continue on its way.
code
python · 16 linesclass Probe:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
print(exc_type, exc_value, type(tb).__name__ if tb else tb)
with Probe():
pass
try:
with Probe():
raise ValueError("annotation batch 47 failed")
except ValueError:
passgo deeper
Memorise the shape __exit__(self, exc_type, exc_value, tb) and the fact that all three are None on a clean exit. That alone lets you write a correct cleanup-only manager.
Explain what each argument is — class, instance, traceback object — and that the triple mirrors sys.exc_info(). Show the if exc_type is None: commit/rollback branch without hesitating.
Demonstrate operational care: BaseException not Exception, never retaining traceback objects in a long-lived process, and knowing that teardown itself failing displaces the original error.
Frame where the with guarantee stops. Cleanup that must survive process death, node loss or a hard kill needs an external reconciliation path; a context manager is an in-process guarantee only.
### The exact signature ```python def __exit__(self, exc_type, exc_value, tb): ... ``` Four parameters counting `self`, no more and no fewer. Python always passes three positional arguments; a method declared `def __exit__(self):` raises `TypeError` at block exit — and the traceback points at the exit, long after the interesting work, which is why arity mistakes here are annoying to debug. ### What the three arguments are * **`exc_type`** — the exception *class*, e.g. `ValueError`. Not an instance, not a name string. This is the value you test. * **`exc_value`** — the exception *instance*: the object that was raised, carrying `args`, any attributes the exception type defines, its `__cause__`/`__context__` chain and its notes. * **`tb`** — a traceback object (`types.TracebackType`), the same object reachable as `exc_value.__traceback__`, describing the frames between the `raise` and the `with`. Together they are exactly what `sys.exc_info()` returns while an exception is being handled, and the desugaring of `with` passes them through unchanged. ### The normal-exit case When the block body ends *without* an exception in flight, Python calls `__exit__(None, None, None)`. That covers more paths than people expect: * falling off the end of the block, * `return` out of the enclosing function from inside the block, * `break` or `continue` out of an enclosing loop, * re-raising handled entirely inside the block by its own `try/except`. So `exc_type is None` is the canonical success test, and it is the branch point for every commit-or-rollback manager ever written: ```python def __exit__(self, exc_type, exc_value, tb): if exc_type is None: self.commit() else: self.rollback() ``` A subtlety worth stating out loud in an interview: `__exit__` is *not* an exception handler. It is told an exception is passing through, and by default it does not stop it. Cleanup runs, then propagation resumes. ### Why three parameters and not one The protocol predates the modern convention that an exception instance carries its own traceback. Since Python 3, `exc_value.__traceback__` and `type(exc_value)` make `exc_type` and `tb` redundant in principle — but the protocol keeps all three for compatibility with the `sys.exc_info()` shape that surrounded it, and every manager in the wild is written to that arity. Do not try to "modernise" it with a one-argument version. ### Using the parameters well Most managers only need `exc_type`. Reach for the others when you have a real use: * **`exc_value`** when the decision depends on the exception's content — an error code, a `.retryable` flag, a status attached with `BaseException.add_note`. * **`tb`** when you are logging or formatting — hand it to the `traceback` module to render frames, or attach it to a diagnostic record. Do not hold onto it beyond the call: a traceback object references every frame in the chain, and every frame's locals, so a manager that stashes tracebacks in a list keeps large object graphs alive. A cleanup-only manager should still declare the parameters and simply not use them; underscore names (`def __exit__(self, exc_type, exc_value, tb)` with the body ignoring them) read better than `*args`, which hides the protocol from anyone reading the class. ### Typing the method With annotations, the honest signature is: ```python import types def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, tb: types.TracebackType | None, ) -> bool | None: ... ``` Note `BaseException`, not `Exception`: a `KeyboardInterrupt` or a `SystemExit` travelling through the block is delivered to `__exit__` too, so a manager that assumes `Exception` is wrong about the type it may see. On Python 3.14 annotations are evaluated lazily by default (PEP 649), so writing these annotations costs nothing at import time even in a hot module. ### Guarantees, and their limit `__exit__` behaves like a `finally` clause. Its guarantee ends where the interpreter's does: an abrupt process termination — a fatal signal, a hard exit that skips normal shutdown — leaves it uncalled. Cleanup that must survive that needs an out-of-process story, not a context manager. Likewise, an exception raised *inside* `__exit__` replaces the one that was in flight, so a teardown that can itself fail deserves its own guard. ### The common interview probe "Print the three arguments for both a clean block and a raising one" is a two-minute exercise that separates people who have written a manager from people who have only used one. The clean run prints three `None`s; the raising run prints the class, the instance and a traceback object.
- Does `__exit__` run when the `with` block exits via `return` or `break`?Yes. The block body sits inside a `try`, and the teardown call is on the `finally` path, so every way out of the block — falling off the end, `return`, `break`, `continue`, or a propagating exception — calls `__exit__` first. On all the non-exception paths the three arguments are `None`, so the manager cannot distinguish a `return` from a clean fall-through, and normally should not need to.
- Why annotate `exc_value` as `BaseException | None` rather than `Exception | None`?Because `KeyboardInterrupt`, `SystemExit` and `GeneratorExit` derive from `BaseException`, not `Exception`, and they travel through a `with` block exactly like any other exception. A manager that assumes it can only see an `Exception` will mistype its own signature and, worse, may reason incorrectly about which failures its teardown must tolerate.
- Is it safe for a manager to store the traceback object it was handed?Not casually. A traceback references its frames, and each frame references its locals, so retaining tracebacks keeps whole object graphs alive — a manager that appends every failure's traceback to a list is a textbook slow leak in a long-running process. Format what you need with the `traceback` module during the call, log the text, and let the object go.
saying these in an interview costs you the question
- Says `__exit__` takes only the exception instance
- Thinks `exc_type` is a string with the exception's name
- Believes `__exit__` runs only when an exception occurred
- Confuses `exc_type` (a class) with `exc_value` (an instance)
- Declares `__exit__(self)` and is surprised by a TypeError
- Assumes only subclasses of `Exception` can arrive