1.3.4 · D5Python Intermediate

Question bank — Context managers — with statement, `__enter__` - `__exit__`

1,432 words7 min readBack to topic

Before the traps, three terms every question below leans on, defined in plain words:


True or false — justify

Return True from __exit__ and the exception vanishes silently.
True — a truthy return tells with "I handled it," so the error is suppressed and code after the block runs as if nothing broke.
Returning None (or nothing) from __exit__ suppresses the exception.
False — None is falsy, so the exception propagates. You must return a genuinely truthy value like True to suppress it.
__exit__ runs even if the body of the with raises an exception.
True — that is the whole point; __exit__ sits in a try/finally, so cleanup always runs whether the body finished normally or crashed.
__exit__ runs even if __enter__ itself raises.
False — if setup fails there is nothing set up to clean, so __enter__'s exception propagates and __exit__ is never called.
The as variable is always the context manager object.
False — the as variable is whatever __enter__ returns, which may be a different object entirely (a string, a number, None).
For with open("f") as f, f is both the manager and the as value.
True but coincidental — a file object returns itself from __enter__, so both roles land on the same object; don't generalise this to other managers.
with is just a fancy try/except.
False — it de-sugars to try/finally (cleanup always runs). It only touches except internally to hand the exception info to __exit__ and decide whether to suppress.
A @contextmanager generator may yield twice for two setup phases.
False — it must yield exactly once; two yields raise RuntimeError: generator didn't stop.
A @contextmanager generator that never yields still works if setup succeeds.
False — zero yields raises RuntimeError: generator didn't yield; the single yield is mandatory and marks the setup/cleanup split.
with a, b: opens both managers and cleans them up in the reverse order.
True — it nests: a enters first and exits last, b enters second and exits first, mirroring stacked resources.
If b.__enter__() raises in with a, b:, then a.__exit__ still runs.
True — a was already fully entered, so its cleanup is guaranteed even though b never opened.

Spot the error

Someone writes __exit__(self) with no other parameters — what breaks?
with calls it as __exit__(exc_type, exc_value, tb), so it fails with a TypeError for missing arguments; __exit__ must accept the three crash-report slots.
return True sits inside a @contextmanager before the yield to suppress errors — why is that wrong?
@contextmanager doesn't read a return value; suppression there is done by wrapping the yield in try/except and not re-raising. A bare return True before the yield just skips setup.
A @contextmanager cleans up with code after yield but no try/finally — what fails?
If the body raises, the exception is thrown into the generator at the yield and the post-yield cleanup is skipped. Only finally (or catching it) guarantees cleanup.
__exit__ returns et (the exception type) instead of a bool to "pass it through" — what actually happens?
A non-None type is truthy, so it suppresses every exception — the opposite of intended. Return an explicit False/None to propagate.
with lock.acquire(): is used to guard a critical section — why does the lock never release?
acquire() returns a bool, not a context manager, so its __enter__ doesn't exist (or the bool has none). Use with lock: — the lock object itself is the manager.
__enter__ forgets return self in a class meant to expose its own attributes — what goes wrong?
The as variable becomes None, so t.elapsed (or any attribute access) raises AttributeError; missing return means the default None is bound.
Inside __exit__, someone does if issubclass(et, ValueError): return True without checking et is not None — when does this crash?
On the normal exit path all three args are None, and issubclass(None, ...) raises TypeError. Guard with et is not None first.

Why questions

Why does with use try/finally rather than try/except?
finally runs unconditionally, guaranteeing cleanup on both the normal and the crash path; except alone would only run on errors.
Why does __exit__ receive three arguments instead of one exception object?
It gets the classic sys.exc_info() triple — type, value, traceback — so it can inspect what kind of error occurred and decide whether to log, clean differently, or suppress.
Why is the return-True-suppresses design a good default rather than a footgun?
Because the default (None, falsy) means errors propagate; you must opt in to swallowing, preventing accidental silent bug-hiding.
Why does moving cleanup next to setup (in one manager) reduce bugs?
The language ties them together so you can't write setup and then forget the matching cleanup — leaked files and deadlocked locks come from exactly that forgetting.
Why can @contextmanager use a single yield to mean "setup here, cleanup there"?
A generator pauses at yield; code before runs on __enter__, and resuming after the block runs the code after yield as __exit__, with the yielded value becoming the as variable.
Why must the cleanup in a @contextmanager sit in finally even though it "looks" like it runs after yield anyway?
If the body raises, that exception is injected at the yield; without finally the post-yield lines are skipped, so cleanup would be lost exactly when it matters most.

Edge cases

What are the __exit__ arguments when the block exits normally?
All three are None(None, None, None) — signalling "no exception occurred."
If both the body and __exit__ raise different exceptions, which one wins?
The exception from __exit__ propagates and the original is attached as its __context__ (chained), so you see the __exit__ error at the top with the original noted underneath.
What happens if the body has a return statement inside a with?
__exit__ still runs before the function actually returns — the try/finally intercepts the return just like it would an exception.
What if __enter__ returns a value but the block never uses as?
The returned value is simply discarded; setup still ran and cleanup is still guaranteed, you just have no handle on the entered object.
Can the same context manager instance be reused in two separate with blocks?
For a plain class, sometimes — but a @contextmanager-based one is single-use, since its generator is already exhausted; reusing it raises. Design for one entry unless you explicitly reset state.
What does returning True from __exit__ do if there was no exception?
Nothing observable — with no exception to suppress, the truthy return has no effect; it only matters when an error is live.

Recall One-line summary of the traps

The two lies this topic tells you: "the as variable is the manager" (no — it's __enter__'s return) and "doing cleanup means the error is handled" (no — only a truthy __exit__ return suppresses).

Connections

  • Exception handling — try-except-finally — the try/finally these traps all orbit.
  • Generators and yield — behind every @contextmanager question.
  • Decorators@contextmanager is one.
  • File I/O — the with open(...) mental model that misleads.
  • Threading and locks — the with lock: vs lock.acquire() trap.
  • Dunder methods — where __enter__/__exit__ live.