A @contextlib.contextmanager caught the with body's error and never re-raised it — what happens?
answer
- Not re-raising is a decision
- The with statement reports success
- A missing bare raise after except
- Green tests can mean silence
- Narrow the caught class, log the suppression
basics
~20 sThe exception is suppressed. Execution continues on the line after the with block as if the body had succeeded, which is how a generator-based manager swallows errors — the equivalent of a class-based manager returning True from its exit method.
solid answer
~40 sA generator-based manager signals suppression by catching the body's exception at the `yield` and simply not re-raising it. The `with` statement then completes normally and the caller sees success, so a failed unit of work is reported as done. That is occasionally intentional and usually a bug: a bare `except Exception:` written for logging, with the `raise` forgotten, converts every failure inside the block into silence. Two related traps: if the generator `yield`s again after catching, you get `RuntimeError: generator didn't stop after throw()`; and if it raises a *different* exception, that one replaces the original, so attach the first as context rather than discarding it. Suppression should be narrow — one named exception class, deliberately, with a comment saying why.
code
python · 14 linesimport contextlib
@contextlib.contextmanager
def swallow():
try:
yield
except ValueError:
pass
finally:
print("cleanup ran")
with swallow():
raise ValueError("boom")
print("execution continued after the with block")go deeper
Know the rule: an except around the yield that does not end in raise makes the error disappear, and the code after the with block runs as if everything worked.
Explain the contrast with finally, which never changes propagation, and be able to say that this is the generator form's equivalent of a class-based manager returning a truthy value from its exit method.
Show how it burns a real system: a batch stage reports success, downstream work runs on partial data, and the suite gets greener because assertions only checked that nothing raised. Name the propagation test that catches it.
Own the standard — every except in a manager ends in raise unless suppression is the documented purpose, narrowly typed and observable through a log or counter, so that silence is never indistinguishable from success.
## The rule Inside a generator-based context manager the `yield` is where the body's exception arrives. What the generator does with it decides everything: * **Does not catch it** — the exception propagates out of the `with`. This is the default and almost always the right behaviour. * **Catches it and re-raises** — same outcome, but the manager got to react on the way past. * **Catches it and returns normally** — the exception is **suppressed**. The `with` statement completes, and the next line after the block runs. That third case is the generator form's equivalent of a class-based manager returning a truthy value from its exit method. There is no flag to return here; *not re-raising is the flag*. ```python import contextlib @contextlib.contextmanager def swallow(): try: yield except ValueError: pass finally: print("cleanup ran") with swallow(): raise ValueError("boom") print("execution continued") ``` Both lines print. The `ValueError` is gone: not logged, not counted, not visible to the caller. ## How it gets written by accident A nightly subscription-billing run makes the shape concrete. Someone wraps each customer's charge in a manager that opens a transaction, and adds an `except Exception:` around the yield so failures can be logged with the customer id — then rolls back, logs, and forgets the bare `raise`. Every subsequent stage behaves as though the charge succeeded: invoices are marked issued, the ledger writer runs against rows that were never committed, and the run exits zero. The defect surfaces days later as an ordering assumption that no longer holds, because a stage that depends on an earlier one now runs against partial data. The test suite does not help. A 27-minute suite whose assertions are all of the form "calling this raises nothing" is *strengthened* by a swallowing manager — the code got quieter, so the tests got greener. The assertion that would have caught it is the one nobody writes: raise a known exception inside the manager's block and assert that the same exception escapes it, with `unittest.TestCase.assertRaises` wrapped around a `with` block whose body raises. ## Related traps at the same yield **Yielding again after catching.** A generator that catches the body's exception and then hits another `yield` produces `RuntimeError: generator didn't stop after throw()`. It usually means someone tried to implement a retry by looping around the yield — a context manager cannot re-run its block, because the block is not inside the generator. Retries belong in a loop around the `with`, not inside the manager. **Replacing the exception.** If the generator raises something new while handling the body's exception, the new one propagates and the original becomes its `__context__`. That is fine when the replacement is more meaningful, but do it deliberately with `raise NewError(...) from exc`, so the chain is explicit rather than accidental. **Suppressing too broadly.** `except Exception` catches `ValueError` and also the `KeyboardInterrupt`-adjacent programming errors you wanted to see — `AttributeError`, `TypeError`, a typo'd name. If suppression is intended, name the class: `except TimeoutError:` says something; `except Exception:` says nothing. ## When suppression is legitimate Sometimes the manager exists precisely to make a class of failure non-fatal — a best-effort cache write, an optional telemetry flush, a cleanup step whose failure must not mask the real error. Two disciplines make that safe. First, keep the caught class as narrow as the intent. Second, make the suppression *visible*: log at warning level with enough identity to find the record later, or increment a counter, so "nothing raised" is not the same as "nothing happened". A manager whose whole job is to suppress one named exception is better expressed with the standard library's dedicated suppression helper than by hand-rolling an `except` around a yield, because the intent is then unmistakable at the call site. ## Reviewing for it The review question is one sentence: *does every `except` in this generator end in `raise`?* If not, the manager suppresses, and that must be intentional and documented. Combined with the `finally` rule — cleanup always, reaction only with a re-raise — it covers essentially every way a generator-based manager can lie to its caller about what happened inside the block. ## Suppression is not cleanup The last thing to keep separate is the pair of jobs a manager can do on the failure path. Cleanup — releasing what was acquired — belongs in `finally` and has no bearing on whether the caller sees the exception. Suppression — deciding the caller should not see it — happens only in an `except` that does not re-raise. Managers that blur the two end up either leaking on error or lying about success, and stating which of the two jobs a given clause is doing is usually enough to settle a review argument about it in one sentence.
- What happens if the generator yields a second time after catching the body's exception?The manager raises `RuntimeError: generator didn't stop after throw()`. It usually means someone tried to build a retry into the manager, but the `with` body is not inside the generator and cannot be re-run. Put the retry loop around the `with` statement instead, or use a decorator that re-invokes the whole function.
- If the generator raises a different exception while handling the body's, which one reaches the caller?The new one. The original becomes its `__context__`, so it still appears in the traceback under "During handling of the above exception". Make that deliberate with `raise WrappedError(...) from exc`, which sets `__cause__` and states the relationship, rather than letting an incidental error inside the handler mask the real failure.
- How would you write a test that catches an accidentally swallowing manager?Enter the manager, raise a known exception inside the block, and assert that the same exception escapes the `with` — plus assert the cleanup side effect happened. A suite whose assertions only check that nothing raised is made greener, not redder, by a swallowing manager, which is why the propagation assertion has to be explicit.
- When is suppressing an exception in a context manager actually the right design?When the manager's purpose is to make one named class of failure non-fatal — a best-effort cache write, an optional telemetry flush, a cleanup step that must not mask the original error. Keep the `except` narrow, and make the suppression observable with a warning log or a counter so silence is not mistaken for success.
saying these in an interview costs you the question
- Thinks finally alone can suppress the exception
- Believes catching without re-raising still propagates the error
- Uses a broad except Exception around the yield for logging
- Tries to retry the with body by yielding again
- Treats a green suite as proof the manager propagates errors
- Discards the original exception when raising a replacement