Exercises — Context managers — with statement, `__enter__` - `__exit__`
This page assumes only the parent note. If a term feels new, it was built there — go back, then return.

The figure above is the mental model for every exercise: with de-sugars into a try/finally, and __exit__ sits in the finally so it always fires. Keep glancing at it.
Level 1 — Recognition
Can you name the pieces and read them off a snippet?
Recall Solution 1.1
(a) The object returned by open("a.txt") — a file object.
(b) f is the return value of __enter__. For files, __enter__ returns the same file object, so f is the file.
(c) __exit__ — it calls .close() on the file when the block is left.
Why: always ask "what does __enter__ return?" — that is the as variable. Here it happens to be the manager itself, but that is a coincidence of the file class.
Recall Solution 1.2
Order: exc_type, exc_value, traceback (mnemonic TVT).
With no exception, all three are None, None, None.
Why: the three args are the "crash report". No crash → nothing to report → three Nones.
Level 2 — Application
Predict the exact output.
Recall Solution 2.1
A
B
C
Why: __enter__ runs first (prints A), then the body (B), then __exit__ on the way out (C). "ENTER sets, EXIT resets."
Recall Solution 2.2
enter
body
exit
caught
Why: the body raises after body, so never is skipped. But __exit__ lives in the finally half of the de-sugaring, so exit still prints. __exit__ returns False, so the ValueError propagates out and is caught by the outer except, printing caught.
Level 3 — Analysis
Reason about return values and swallowing.
Recall Solution 3.1
after does not print — the KeyError propagates and crashes the program (unless caught elsewhere).
Why: __exit__ returned None. None is falsy, and the de-sugaring says re-raise unless __exit__ returned a truthy value. Only an explicit return True (or any truthy value) swallows the error. Returning None — the default when you write no return — lets it propagate.
Recall Solution 3.2
(a) x equals 42 — the yielded value becomes the as variable.
(b)
setup
body 42
cleanup
(c) No. The manager is a generator object created by band(); x is the integer 42. They are different objects — this is exactly the L1 trap in action.
Why: with @contextmanager, code before yield is __enter__, the yielded value is what __enter__ returns, and code after yield is __exit__.
Level 4 — Synthesis
Build a working manager from scratch.
Recall Solution 4.1
class ignore:
def __init__(self, *exc):
self.exc = exc
def __enter__(self):
return self
def __exit__(self, et, ev, tb):
return et is not None and issubclass(et, self.exc)Output: still here prints.
Why each line:
et is not None— an exception only occurred ifet(the type) is set; if it'sNonewe must return falsy so a clean exit isn't wrongly reported as "suppressed".issubclass(et, self.exc)—Trueonly for the types we asked to ignore. For those,__exit__returnsTrue→ thewithswallows the error and execution continues toprint.- A different error (say
PermissionError) makesissubclassreturnFalse→ propagates.
Recall Solution 4.2
import time
from contextlib import contextmanager
@contextmanager
def timer():
tic = time.perf_counter()
box = {} # mutable container we can fill later
try:
yield box # caller reads box["elapsed"] AFTER the block
finally:
box["elapsed"] = time.perf_counter() - tic
with timer() as t:
sum(range(1_000_000))
print(t["elapsed"]) # e.g. 0.01Why a plain number fails: if you yield 0.0, the caller binds t = 0.0 at enter time, before any work has run. You cannot go back and change the integer the caller already holds. A mutable container (dict/list) lets __exit__-side code fill in the true elapsed value after the body finishes, and the caller — holding the same object — sees the update.
Why finally: so box["elapsed"] is set even if the body raises.
Level 5 — Mastery
Compose managers and reason about nesting order.
Recall Solution 5.1
enter 1
enter 2
body
exit 2
exit 1
M(2)'s __exit__ runs first. Managers unwind last-in, first-out (LIFO), like nested boxes: the inner one opened last, so it closes first.
Why: with M(1), M(2): is sugar for nesting with M(1): with M(2):. The inner block's finally fires before control returns to the outer block's finally.
Recall Solution 5.2
pass sees ValueError
swallow sees ValueError
done
done prints.
Why: the ValueError first hits the inner Pass.__exit__, which sees it but returns False → the error keeps propagating outward. It then reaches the outer Swallow.__exit__, which returns True → the error is suppressed there. Both __exit__s ran (LIFO), and because the outermost one swallowed the exception, execution resumes and done prints.
Key insight: an exception "walks outward" through each enclosing __exit__ until one returns truthy. If none does, it escapes the whole with stack.
Recall Self-check summary
__enter__return value →asvariable ::: yes__exit__truthy return → suppress ::: yesNonereturn → propagate ::: yes- Managers unwind LIFO ::: yes
Connections
- Exception handling — try-except-finally — every exercise here is really a
try/finallyin disguise. - Generators and yield — behind Exercises 3.2 and 4.2.
- Decorators —
@contextmanageris the decorator doing the work. - File I/O — the
open(...)in Exercises 1.1 and 4.1. - Threading and locks — the LIFO unwinding of Exercise 5.1 is why
with lock:nesting is safe. - Dunder methods —
__enter__/__exit__are protocol dunders.