skip to content

What does returning True from a context manager's __exit__ do to the exception?

level: middleimportance: must knowfreq 62%

answer

  1. One output, one meaning
  2. Truthy versus falsy, not True versus False
  3. Missing return statement is the safe direction
  4. Control resumes after the block, not the raise
  5. Check exc_type before returning truthy

basics

~10 s

A truthy return from exit swallows the exception: it is discarded and execution resumes on the line after the with block, not where the error was raised. Returning None or False lets it propagate.

solid answer

~40 s

The return value of `__exit__` is the suppression switch. Truthy means "I handled this exception, discard it", and the program continues at the statement *after* the `with` block - the rest of the body is skipped, because the frame already unwound to the block's end. `None`, `False`, `0` or `""` all mean "keep unwinding". The dangerous habit is an unconditional `return True`, often written while thinking of it as "cleanup succeeded": it swallows everything the body can raise, including `AttributeError` and `TypeError` from your own bugs, and turns a failing job into a silently succeeding one. A responsible suppressing manager inspects `exc_type` first and returns truthy only for the narrow set of exception classes it genuinely handles. When no exception occurred, `exc_type` is `None` and the return value is ignored entirely.

code

python · 13 lines
python
class SuppressValueError:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return exc_type is not None and issubclass(exc_type, ValueError)


with SuppressValueError():
    raise ValueError("swallowed")
    print("never reached")

print("execution resumes after the with statement")

go deeper

for a junior

Memorise the switch: truthy return means the exception is thrown away, anything falsy - including the None you get by not returning - means it keeps going. Be able to say which one a normal cleanup manager does.

for a middle

Explain that falsy covers None, False, 0 and "", that control resumes after the whole block rather than after the raise, and that a correct suppressing manager tests exc_type before returning truthy.

for a senior

Demonstrate the operational judgement: an unconditional truthy return hides your own bugs and can swallow KeyboardInterrupt, and any manager that suppresses must report the skipped work on another channel so a batch job cannot exit clean while doing nothing.

for a principal

Own the policy. Decide where in the codebase suppression is allowed at all, require that suppressing managers name their exception classes and emit a counter or warning, and treat a silent-success manager in shared code as a defect worth a review rule.

`__exit__` has one output and it means exactly one thing: **do I stop this exception here?** Everything else the method does - closing handles, rolling back, logging, measuring - is a side effect. The return value is the contract. ## The rule ``` truthy return -> the exception is discarded; execution continues after the with block falsy return -> the exception is re-raised and keeps propagating ``` Falsy includes `None`, `False`, `0`, `0.0`, `""`, `[]` and `{}`. Since a Python function with no `return` statement yields `None`, forgetting to return is the safe direction - you propagate. That asymmetry is intentional. ## Where control lands after suppression This is the part interviewers probe. Suppression does **not** resume the body at the line after the `raise`. The exception already unwound the block, so the next statement executed is the first one *after* the whole `with` statement. Anything left in the body is skipped: ```python with SuppressValueError(): step_one() raise ValueError("boom") step_two() # never runs print("here") # this is where control lands ``` If a candidate says "the loop keeps going" they need to be precise about which loop: a `with` inside a `for` body still ends *that* iteration's `with` block and continues with the next iteration; a `with` wrapping a `for` loop kills the whole loop. ## The unconditional-True bug The realistic failure looks like this: ```python class Batch: def __exit__(self, exc_type, exc, tb): self.conn.close() return True # "cleanup worked" ``` The author meant "my teardown succeeded". What they wrote is "no error inside this block will ever be seen again". A misspelled attribute, a `TypeError` from a bad call signature, a `KeyError` from a missing configuration key - all of them vanish, and the process exits 0. That is worse than a crash, because the crash is at least visible. Write the check explicitly instead: ```python def __exit__(self, exc_type, exc, tb): self.conn.close() return exc_type is not None and issubclass(exc_type, ValueError) ``` Now the intent is legible: this manager handles bad input values and nothing else. ## Do not suppress BaseException `exc_type` can be something you must never swallow. `KeyboardInterrupt`, `SystemExit` and `GeneratorExit` derive from `BaseException`, not `Exception`. A manager that returns `True` for everything makes Ctrl-C stop working inside its block and can keep a shutdown from proceeding. Test with `issubclass(exc_type, Exception)` as the outer bound if you are suppressing broadly, and prefer naming the specific classes. ## The no-exception case When the body completes normally, `__exit__` is still called, with all three arguments set to `None`, and the return value is **ignored**. So `return True` does nothing on the success path - which is exactly why the mistake is easy to make and hard to notice: the manager works fine in every test that does not raise. ## Suppression is a decision about the caller The deeper point, and the one that separates a middle answer from a senior one: `__exit__` is not deciding what to do about the error, it is deciding what the *caller* gets to know. Code above the `with` block can no longer distinguish "the work completed" from "the work blew up and someone swallowed it". Every suppressing manager therefore owes the caller a signal on some other channel - a flag on the manager object, a counter, a warning log line - so a nightly job can report "3,412 records processed, 118 skipped" instead of exiting clean on a corrupted input file. ## Quick self-check for an interview State the rule, name the falsy default, say where execution resumes, and volunteer the hazard: an unconditional `return True` hides your own bugs. That fourth part is what the question is actually testing - the mechanics are two sentences, and the judgement is the rest. ## Truthiness, not True Because the interpreter tests the value rather than comparing it to `True`, a manager that accidentally returns *data* becomes an intermittent suppressor. A method ending in `return self.errors` swallows the exception on exactly the runs where errors were collected, and propagates on the runs where the list is empty - failure behaviour that depends on the input, which is close to the worst possible bug shape. The same applies to `return len(self.records)`, `return self.handle` or any refactor that adds a value-returning line at the end of `__exit__`. Keep the method's last statement an explicit boolean expression, and prefer writing the class check inline - `return exc_type is not None and issubclass(exc_type, (ValueError, KeyError))` - so a reader can see the complete suppression policy on one line.

  • After __exit__ suppresses an exception, where exactly does execution resume?
    On the first statement after the entire `with` block. The remainder of the body is skipped, because the exception had already unwound the block before `__exit__` was consulted. If the `with` sits inside a `for` loop, that iteration ends and the next one starts; if the `for` loop sits inside the `with`, the whole loop is abandoned.
  • Why is an unconditional `return True` in __exit__ considered a defect rather than a style choice?
    Because it suppresses exception classes the manager never reasoned about - `AttributeError`, `TypeError`, `KeyError` from ordinary bugs - and can swallow `KeyboardInterrupt` and `SystemExit` too, since they derive from `BaseException`. The failure mode is a job that exits successfully having done nothing, which is far more expensive to diagnose than a traceback.
  • What does __exit__'s return value do when the with body completes without raising?
    Nothing. `__exit__` is still called, with `exc_type`, `exc_value` and `traceback` all `None`, and the return value is ignored because there is no exception to suppress. This is why a stray `return True` passes every happy-path test and only shows up the first time production raises inside the block.
  • If a manager legitimately suppresses an exception class, how should the caller learn that work was skipped?
    Through a separate channel the manager owns: a boolean or counter attribute the caller reads after the block, a warning-level log record, or a metric. Suppression removes the caller's only automatic signal, so a manager that swallows without recording anything turns a partial failure into a silent success.

saying these in an interview costs you the question

  • Says returning False suppresses the exception
  • Thinks execution resumes at the line after the raise
  • Writes return True unconditionally as cleanup confirmation
  • Believes only literal True suppresses, not other truthy values
  • Suppresses BaseException, breaking Ctrl-C and shutdown
  • Assumes the return value matters on the success path

context