Visual walkthrough — Context managers — with statement, `__enter__` - `__exit__`
This is the companion visual walkthrough to the parent topic. Every arrow you see in the code, we'll first draw as a picture.
Step 1 — The problem: two things that must always come in pairs
WHAT. Some resources come in pairs of actions: open a file → close it; grab a lock → release it; start a timer → stop it. The first action acquires, the second releases.
WHY. If you acquire but forget to release, the resource leaks. A never-closed file eats an OS handle; a never-released lock freezes your whole program (a deadlock — two pieces of code each waiting forever for the other).
PICTURE. Think of a door. Walking in = acquire. Walking out = release. The danger is a door you walked through but never walked back out of.

Step 2 — The naive fix, and the hole in it
WHAT. The obvious idea: release right after you're done.
f = open("data.txt") # acquire
data = f.read() # use it
f.close() # releaseWHY it's not enough. Look at the middle line. If f.read() raises an error, Python jumps past f.close() — the release never runs. So the "obvious" version leaks the file precisely when something goes wrong, which is the worst time.
PICTURE. The error is a trapdoor in the middle of the room. You fall through it and never reach the exit door. The file stays open forever.

Step 3 — try / finally: a release that cannot be skipped
WHAT. Python has a tool built for exactly this: try / finally. Whatever happens in the try block, the finally block runs on the way out.
f = open("data.txt") # acquire (outside try — if this fails, nothing to close)
try:
data = f.read() # use it — may raise
finally:
f.close() # release — runs no matter whatTerm by term:
try:— "attempt this, and be ready for it to blow up."finally:— "no matter how you leave the try block (normally, or by exception, or byreturn), run this first."
WHY this shape. f.close() in finally is a promise. The trapdoor from Step 2 no longer helps the file escape cleanup — even falling through it routes you through finally.
PICTURE. Now the exit door is placed so that every path — the normal walk-out and the trapdoor — funnels through it before you leave the building.

Step 4 — Naming the two halves: __enter__ and __exit__
WHAT. We give the pair of actions official method names, so any object can play this game — not just files.
Look at __exit__'s three arguments — this is the "crash report" Python hands the cleanup code:
If the block finished without an error, all three are None. That is how __exit__ knows whether anything went wrong.
WHY methods and not just two functions. Because now the acquire/release pair travels inside one object. You hand that object to with once; it carries both halves with it.
PICTURE. The context-manager object is a little box with two buttons: a green ENTER button (setup) and a red EXIT button (cleanup, which also receives the crash report).

Step 5 — Assembling with: the exact de-sugaring
WHAT. Now we glue Steps 3 and 4 together. Here is the full translation of
with EXPR as VAR:
BODYinto plain try/finally — this is the definition of what with does:
mgr = EXPR # (a) build the context-manager object
VAR = mgr.__enter__() # (b) acquire; return value → the `as` variable
try:
BODY # (c) your code runs here
except BaseException: # (d) something raised inside BODY
if not mgr.__exit__(*sys.exc_info()): # (e) hand it the crash report
raise # (f) re-raise unless __exit__ said "swallow it"
else: # (g) BODY finished cleanly
mgr.__exit__(None, None, None) # (h) release; no error → three NonesLine by line, matched to the pictures we already built:
- (a) evaluate
EXPRonce → the box from Step 4. - (b) press the green button. Whatever it returns is what
as VARbinds — not the box itself (a classic trap; see the mistake below). - (c)–(d) if
BODYblows up, we catch every exception (BaseExceptionis the root of all of them). - (e) press the red button, feeding it the crash report.
sys.exc_info()is exactly the tuple(exc_type, exc_value, traceback). - (f)
__exit__'s return value is a yes/no flag: "should I swallow this error?".if not (...)means: if it did not say yes, re-raiseand let the error keep travelling. - (g)–(h) no error path: still press the red button, but the crash report is all
None.
WHY this exact shape. It guarantees __exit__ runs on every exit path (the funnel from Step 3), and it lets cleanup decide the error's fate (the return-value flag).
PICTURE. Two roads leave BODY — the calm road (no error) and the storm road (error). Both roads pass through the red EXIT button before leaving; only the storm road carries a crash report.

Step 6 — The return-value flag decides the error's fate
WHAT. Zoom into line (e)–(f). The single most misunderstood rule of context managers lives here:
WHY None propagates. A method with no explicit return returns None, and None is falsy. So if not mgr.__exit__(...) becomes if not None → if True → we raise. Doing your cleanup does NOT hide the error — you must explicitly return True to hide it. This is a safety feature: silent error-swallowing is a top bug source.
PICTURE. The red button is also a gate. return True closes the gate — the error is trapped inside and vanishes. return None/False leaves the gate open — the error walks straight out and up the call stack.

Step 7 — The degenerate & edge cases (never leave a gap)
We must show every path a reader could hit.
Case A — BODY finishes normally. Road (g)→(h): __exit__(None, None, None). Cleanup still runs.
Case B — BODY raises, __exit__ returns falsy. Road (d)→(e)→(f): cleanup runs, then the error re-raises. Normal, expected behaviour.
Case C — BODY raises, __exit__ returns True. Road (d)→(e): cleanup runs, error suppressed, program continues after the with.
Case D — __enter__ itself raises (acquire fails). Look at line (b): it is before the try. So __exit__ never runs — and correctly so: you never acquired the resource, so there is nothing to release. You can't close a file you never opened.
Case E — EXPR isn't a context manager. No __enter__/__exit__ → AttributeError at line (a)/(b), before BODY ever runs.
Case F — @contextmanager with the wrong number of yields. The generator must ==yield exactly once==. Zero yields → RuntimeError: didn't yield. Two yields → RuntimeError: generator didn't stop. The single yield is the boundary between the two roads.

The one-picture summary
Everything above, compressed: the box has two buttons; with presses green, runs your body, then always presses red — and the red button's return value decides whether an error escapes.

Recall Feynman: the whole walkthrough in plain words
Some things come in pairs — open/close, lock/unlock. If you forget the second half, the program leaks or freezes. Writing the release yourself is risky because an error can jump past it. Python's finally fixes that: it runs on every way out of a block. We wrap that pattern into an object with two buttons — a green "set up" button (__enter__, whose result becomes your as variable) and a red "clean up" button (__exit__, which is always pressed and is handed a crash report of Type/Value/Traceback). The with statement just presses green, runs your code, and guarantees it presses red on the way out — whether your code finished calmly or crashed. The red button can return True to swallow the crash, or None/False to let it fly on. And if the green button itself fails, red is never pressed, because you never actually acquired anything to release.
Recall
Which de-sugared line binds the as variable?
VAR = mgr.__enter__() — the return value of __enter__Does __exit__ run if __enter__ raises?
__enter__ is before the try, so nothing was acquired to release__exit__ returns None after an error — what happens?
None is falsy → the if not re-raises)What are sys.exc_info()'s three parts?
__exit__How many times must a @contextmanager generator yield?
yield splits setup from cleanupConnections
- Exception handling — try-except-finally — the
try/finallyengine underneath every step here. - Generators and yield — the single-
yieldsplit of Case F. - Decorators —
@contextmanageris a decorator around a generator. - File I/O — the canonical acquire/release pair (open/close).
- Threading and locks —
with lock:guarantees the release in Step 1. - Dunder methods —
__enter__/__exit__belong to this protocol family.