Visual walkthrough — Exception handling — try, except (specific), else, finally
We build a running example and watch it three ways: no error, caught error, uncaught error. Nothing is assumed — we define every word the first time it appears.
Step 0 — What is an "exception", really?

Look at the figure: the top cyan pipe is the normal channel; the amber drop-down arrow is the moment of raise. The whole rest of the walkthrough is: where does that amber arrow land?
Step 1 — The stage: four blocks stacked as chutes
WHAT. We lay out the four blocks —
try,except,else,finally— as four horizontal shelves, in the exact order Python reads them.
WHY. Control flow only ever moves between these shelves. If we can draw the shelves once and mark the legal jumps between them, every future example is just "trace the arrows".
PICTURE. Each shelf holds the statements from the parent's skeleton:

In the figure, notice finally is drawn as a turnstile at the bottom: no matter which shelf you were standing on, you must pass through it to leave.
Step 2 — The clean run (no exception)
WHAT. Run
tryall the way to the end with nothing going wrong, then follow where the flow goes next.
WHY. This is the baseline. Before we study failure we must know the shape of success — otherwise we can't tell what failure changed.
PICTURE. The flow stays in the top pipe through every
trystatement, then hops toelse, then down throughfinally.
Concrete code:
try:
n = int("42") # S1: succeeds, n = 42
except ValueError:
print("bad") # SKIPPED — no error to catch
else:
print(n) # runs: prints 42
finally:
print("done") # runs: prints doneOutput order: 42, then done.

The green path in the figure never dips into the error channel. except is greyed out — unreached.
Step 3 — The caught run: the flow drops into the error channel
WHAT. Now statement
S1raises. Watch the flow abandon the rest oftry, drop into the error channel, and get stopped by a matchingexcept.
WHY. This is the whole point of exception handling: a mid-block failure must not keep running the following statements (rule 1 of the parent). We visualise exactly which statements are skipped.
try:
n = int("oops") # S1: RAISES ValueError here
print("unreachable") # S2: SKIPPED — never runs
except ValueError: # filter opens: ValueError IS-A ValueError
print("bad input") # runs
else:
print(n) # SKIPPED — try was NOT clean
finally:
print("done") # runsOutput: bad input, then done.

In the figure the amber arrow drops at S1; S2 and else are crossed out; except glows because its filter matched.
Step 4 — Why else, not "end of try"? (the small-region argument)
WHAT. Move the success line
print(n)insidetryversus keeping it inelse, and compare what an accidental error does.
WHY. The parent claimed
elsekeeps the guarded region "as small as possible". Let's see the bug thatelseprevents.
PICTURE. Two side-by-side chutes. On the left, the success line lives in
try, so a different error it raises falls into the sameexcept— a mask. On the right,elsesits outside the guarded region, so its errors escape pastexcept.
# LEFT — dangerous: success line inside try
try:
n = int("42")
print(undefined_var) # raises NameError — a BUG
except ValueError:
print("bad") # This except is for VALUE errors...
# ...but NameError is not caught here, it propagates — good luck if you'd
# written a broader except: it would SILENTLY swallow your typo.# RIGHT — safe: success line in else
try:
n = int("42") # ONLY the risky parse is guarded
except ValueError:
print("bad")
else:
print(undefined_var) # NameError raised OUTSIDE the guard → not caught → visible
Notice in the figure: the amber "bug" error on the right shoots straight past the except filter (it's a NameError, not a ValueError), because it was raised outside the guarded box. On the left it's trapped inside the box — dangerous.
Step 5 — Ordering except clauses: child before parent
WHAT. Two
exceptclauses, one for a parent class (LookupError) and one for its child (KeyError). See which one "grabs" the error.
WHY. The filter test is
isinstance, which is True for subclasses too. So a broad filter placed first will shadow a narrow one below it — the narrow one becomes dead code.
PICTURE. The error object falls past the clauses top-to-bottom; the first filter that accepts it stops it. A wide net placed high catches everything.
try:
d = {}
d["k"] # raises KeyError
except LookupError: # KeyError IS-A LookupError → filter accepts FIRST
print("lookup failed") # runs
except KeyError:
print("never reached") # DEAD CODE — parent above already caught it
See The exception class hierarchy — BaseException, Exception, LookupError for the full family tree. In the figure the wide amber net (LookupError) sits above the narrow net (KeyError) and catches the falling ball first.
Step 6 — finally is a turnstile: it runs even on return
WHAT. Put a
returninsidetry. Show thatfinallystill runs — before the function actually hands back its value.
WHY. People assume
returnmeans "leave immediately". But rule 5 says every exit passes throughfinally. This is the degenerate case that surprises everyone.
PICTURE. The
returnvalue is "packed into a box" and held at the turnstile;finallyruns; then the box is released.
def f():
try:
return "from try" # value packed, but not yet delivered
finally:
print("cleanup!") # turnstile: runs BEFORE the value leaves
# Calling f(): prints "cleanup!" then returns "from try"
The amber box labelled "from try" waits at the turnstile in the figure while cleanup! prints, then slides out.
Step 7 — The uncaught run: propagation and crash
WHAT. Raise an error that no
exceptfilter accepts. Trace it out of this construct, up the call stack, to a crash.
WHY. We promised to cover every case (rule 4). The "nobody catches it" case is where a traceback comes from — and
finallystill runs on the way out.
PICTURE. The error ball rolls past all filters, drops through
finally(which still fires), then keeps falling out of the function into the caller — and if the caller has no handler either, off the bottom = crash.
try:
x = 1 / 0 # raises ZeroDivisionError
except ValueError: # filter REJECTS: not a ValueError
print("bad")
finally:
print("cleanup") # STILL runs on the way out
# then: ZeroDivisionError propagates → traceback → crash (if never caught)Output: cleanup, then the program crashes with a ZeroDivisionError traceback.

In the figure the ball misses every filter, still touches the finally turnstile (it glows), then falls off the bottom into the caller — see Debugging and tracebacks for reading the crash report that results.
The one-picture summary
WHAT. All three journeys — clean, caught, uncaught — on one map, sharing the same
finallyturnstile.

Recall Feynman retelling — the whole walkthrough in plain words
Picture your code as a top pipe (normal work) and a bottom pipe (error alarm). Statements run calmly along the top of the try shelf.
If nothing breaks, the flow slides off the end of try, hops onto the else shelf (the "all clear" one), and then goes through the finally turnstile on its way out. The except filter never even wakes up.
If something breaks, the flow drops into the bottom pipe the instant the error is born. Everything below that point in try is skipped. The error ball rolls past the except filters top-to-bottom; the first filter that says "yes, I accept this type" grabs it and runs its handler. Because filters accept subclasses too, you always list the specific ones first, or a wide parent net catches everything and your specific handler becomes dead code. After the handler, the flow exits through the finally turnstile.
If no filter accepts the ball, it keeps falling — but it still touches the finally turnstile on the way (cleanup is guaranteed even in a crash), then drops out of this function into whoever called it. If nobody up the stack catches it, it falls off the bottom of the whole program and you get a traceback.
And the turnstile is stubborn: even a return inside try has to pass through finally before its value is delivered. Everything leaves through finally — that's the one shelf on every path.
Connections
- Parent: Hinglish version of the parent
- The exception class hierarchy — BaseException, Exception, LookupError — why
isinstancematching orders your clauses. - Raising exceptions — raise and custom exception classes — how the error object in Step 0 is born.
- Context managers — with statement and __enter__/__exit__ — a cleaner replacement for the
try/finallyturnstile. - File handling in Python — the classic place
finally(orwith) closes resources. - Debugging and tracebacks — reading the crash from Step 7.
- Logging vs printing errors — what to do inside the
excepthandler.