Worked examples — Exception hierarchy
You already met the family tree of exceptions: BaseException at the very top, Exception for "normal" errors, and specific leaves like KeyError. This page does one job — it drags every distinct kind of situation into the light and works each one by hand, so you never meet a case in real code that you have not already seen here.
The scenario matrix
Before any code, let us list every class of situation an exception-handling block can face. Each later example is tagged with the exact example number that fills it.
| # | Case class | What is special about it | Example |
|---|---|---|---|
| A | Exact leaf match | handler names the precise exception | Ex 1 |
| B | Parent catches child | handler names an ancestor, child rolls up to it | Ex 1, 2 |
| C | Sibling divergence | two children of one parent, handled together | Ex 2 |
| D | Order matters (dead code) | general-before-specific → later handler unreachable | Ex 3 |
| E | Degenerate / zero input | zero divisor, empty container, None |
Ex 1, 4 |
| F | Limiting value | float result past the range ceiling → inf, not an error |
Ex 4 |
| G | BaseException trap |
Ctrl+C / SystemExit slipping through a too-broad catch |
Ex 5 |
| H | No handler at all | ball rolls off the top → crash | Ex 6 |
| I | Real-world word problem | file that may be missing or unreadable | Ex 7 |
| J | Bare / tuple catch-all | except: and except (E1, E2): idioms |
Ex 8 |
| K | Exam twist | else/finally + re-raise |
Ex 9 |
The rest of the page fills every one of cells A–K.
Cells A, B, E — Arithmetic branch

Cells B, C — Sibling divergence under LookupError

Cell D — Order matters (dead code)
Cells E, F — Zero-division vs the float ceiling

inf (cells E, F)
Two look-alike operations that behave very differently:
import math
def kinds(a, b):
try:
out = a / b
if isinstance(out, float) and math.isinf(out):
return "inf" # overflowed silently to infinity
return out
except ZeroDivisionError:
return "zerodiv"Call kinds(1, 0) and kinds(1e308, 1e-10).
Forecast: does the huge-number call return a number, "inf", or crash?
-
kinds(1, 0)→ dividing by zero raisesZeroDivisionError→"zerodiv"(cell E). Why this step? The degenerate divisor case; there is genuinely no answer, so Python raises. -
kinds(1e308, 1e-10)→ the true value would be a number about 318 zeros long, far past the biggest float Python can store (roughly1.7977e308, drawn as the red ceiling in figure s03). Key correction: ordinary float division does not raiseOverflowErrorhere — CPython quietly returns the special valuefloat('inf')("infinity"). So the call returns"inf", never an exception (cell F, limiting value). Why this step? This is the trap: passing the ceiling is not an error you canexcept; the number silently becomesinf. You must test forinfyourself withmath.isinf, which is exactly what theifline does. -
Contrast:
10**1000 / 10**999uses big integers, promotes cleanly, and simply equals10.0— no ceiling involved. Why this step? Proves the ceiling is a float-range fact, not an "answer too big" fact.
It is tempting to write except OverflowError after a plain /. But float division ==overflows to inf silently==, so that handler never fires for 1e308/1e-10. Guard with math.isinf(x) instead of an except.
Verify: kinds(1,0) == "zerodiv"; 1e308/1e-10 == float('inf'); math.isinf(1e308/1e-10) is True; 10**1000/10**999 == 10.0.
Cell G — The BaseException trap

except Exception and not except BaseException (cell G)
def guarded(fn):
try:
return fn()
except Exception: # note: NOT BaseException
return "handled"Feed it two "errors": a normal one, and a KeyboardInterrupt.
Forecast: which one gets swallowed, which one escapes?
-
guarded(lambda: 1/0)→ZeroDivisionErroris-aException→ caught →"handled". Why this step? Normal errors live belowException, so the net catches them. -
Now imagine
fnraisesKeyboardInterrupt(the object Ctrl+C creates). In figure s04,KeyboardInterrupthangs offBaseExceptionbesideException, not under it. The ball rolls up its own branch, missesexcept Exception, and propagates out. Why this step? This is the whole point of the split:except Exceptiondeliberately lets termination signals escape so Ctrl+C still stops your program. -
Had we written
except BaseException, we would have caught the interrupt too — the program would ignore Ctrl+C. That is cell G: the too-broad catch. Why this step? Fixes the "catch everything" instinct with the correct boundary.
==Exception = my bugs. BaseException = the OS talking to my program (exit, Ctrl+C).== Catch the first; leave the second alone unless you are a logging framework.
Verify (structural): issubclass(ZeroDivisionError, Exception) is True; issubclass(KeyboardInterrupt, Exception) is False; issubclass(KeyboardInterrupt, BaseException) is True.
Cell H — No handler: the crash case
def unhandled():
try:
return [][0] # IndexError
except KeyError:
return "key"unhandled() — return value or crash?
Forecast: does the except KeyError help at all?
[][0]on an empty list raisesIndexError. Why this step? Empty-container degenerate access — a common real bug.- The only handler names
KeyError.IndexErroris not aKeyErrorand is not its child — different branch. The ball propagates past this handler. Why this step? A handler only catches its own exception and that exception's descendants — never a sibling. - No further handler exists, so the
IndexErrorreaches the top and crashes the function with a traceback. Why this step? Cell H is the reminder that atryblock does not magically make code safe — only matching handlers do.
Verify: calling unhandled() raises IndexError (checked by asserting the raise).
Cell I — Real-world word problem
A program must read config.txt. Three real outcomes: it reads fine, it is missing, or it exists but the OS denies permission.
def read_config(exists, readable):
try:
if not exists:
raise FileNotFoundError
if not readable:
raise PermissionError
return "contents"
except FileNotFoundError:
return "default" # recover with a default config
except PermissionError:
return "denied"
except OSError:
return "os-safety-net"Evaluate (True,True), (False,True), (True,False).
Forecast: three inputs, three outputs — name them.
(True, True)→ file present and readable → returns"contents". Happy path. Why this step? Baseline where nothing throws.(False, True)→FileNotFoundErrorraised; caught by its own leaf handler →"default". We recover, not crash. Why this step? Missing file is expected in the real world, so it deserves specific recovery — exactly what the hierarchy lets us target.(True, False)→PermissionErrorraised → caught by its own handler →"denied". BothFileNotFoundErrorandPermissionErrorare children ofOSError, so the finalexcept OSErrorstays as a safety net for the other 20+ OS errors (disk full, dropped network drive). Why this step? Cell I combines cells A (two exact leaves) and B (parent net) in one realistic block.
Verify: returns "contents", "default", "denied" for the three inputs.
Cell J — Bare catch-all and the tuple syntax
except: vs except (E1, E2): (cell J)
Two idioms you will meet constantly. First, the tuple form catches several specific types in one clause:
def tuple_catch(x):
try:
return 10 / x # ZeroDivisionError if x==0
except (ZeroDivisionError, TypeError): # tuple = "any of these"
return "bad-input"Second, the bare form catches literally everything, like except BaseException::
def bare_catch(x):
try:
return 10 / x
except: # bare — catches Ctrl+C too!
return "swallowed"Call tuple_catch(0), tuple_catch("a"), and bare_catch(0).
Forecast: what does each return, and which idiom is dangerous?
-
tuple_catch(0)→10/0raisesZeroDivisionError, which is one of the listed types →"bad-input". Why this step? The tuple means "match if the ball is any of these branches" — a clean way to give two sibling-ish errors the same recovery without a shared parent. -
tuple_catch("a")→10 / "a"raisesTypeError(can't divide by a string), also in the tuple →"bad-input". Why this step? Shows both members of the tuple actually fire; it is not just decoration. -
bare_catch(0)→ZeroDivisionErroris caught →"swallowed". But a bareexcept:is equivalent toexcept BaseException:— it would also swallowKeyboardInterruptandSystemExit. Same danger as cell G. Why this step? Cell J's warning: prefer the tuple form for precision; avoid the bare form for exactly the reasons Example 5 gave.
except: bare in real code
==A bare except: catches BaseException,== so it eats Ctrl+C and sys.exit(). Use except Exception: for a safe net, or a tuple except (A, B): for targeted catches.
Verify: tuple_catch(0) == "bad-input"; tuple_catch("a") == "bad-input"; bare_catch(0) == "swallowed".
Cell K — Exam twist: else, finally, and re-raise
log = []
def full_flow(x):
try:
y = 10 / x
except ZeroDivisionError:
log.append("except")
raise ValueError("bad x") # re-raise a DIFFERENT error upward
else:
log.append("else") # runs ONLY if no exception
return y
finally:
log.append("finally") # runs ALWAYS, lastRun full_flow(2), then call full_flow(0) and catch whatever it re-raises. Then inspect log.
Forecast: in which call does "else" appear, where does "finally" land, and what error escapes full_flow(0)?
full_flow(2)→10/2 = 5.0, no exception. Theelseblock runs (loggets"else"), thenfinallyruns (loggets"finally"), and the function returns5.0. Why this step?elseis the "thetrysucceeded" branch — it runs only when the body threw nothing. Exams love testing thatelseis skipped wheneverexceptfires.full_flow(0)→10/0raisesZeroDivisionError; theexceptruns (loggets"except") and then re-raises a newValueError. Theelseis skipped (an exception occurred), butfinallystill runs (loggets"finally") before theValueErrorleaves the function. Why this step? This is the re-raise pattern: catch a low-level error, translate it into a meaningful one for the caller, yetfinallycleanup still fires on the way out. Callers must therefore wrapfull_flow(0)in their owntryexpectingValueError.- Catching that escaping error at the call site:
Why this step? Confirms the re-raised type/message is what actually propagates — not the originaltry: full_flow(0) except ValueError as e: caught = str(e) # "bad x"ZeroDivisionError. - After both calls,
log == ["else", "finally", "except", "finally"]. Why this step? Reading the whole log proves the ordering rule:try → (except OR else) → finally, and thatfinallyruns even when an exception is on its way out (see context managers and 1.3.06-TryExcept-Finally for the same guarantee packaged differently).
Verify: full_flow(2) == 5.0; full_flow(0) raises ValueError with message "bad x"; final log == ["else", "finally", "except", "finally"].
Recall Quick self-test
Handler order rule ::: most-specific first, most-general last, else children become dead code
except Exception catches Ctrl+C? ::: No — KeyboardInterrupt is under BaseException, beside Exception
Two siblings sharing one handler ::: name their common parent, e.g. LookupError for IndexError + KeyError
Catch several unrelated types in one clause ::: except (E1, E2): tuple form
Danger of a bare except: ::: it equals except BaseException: and swallows Ctrl+C / sys.exit()
When does else run ::: only when the try body raised nothing
When does finally run ::: always, even after a return or a re-raise inside except
Float division past the range ceiling ::: returns float('inf') silently — no exception; test with math.isinf
Where to go next
- Build your own leaves: 1.3.08-Custom-Exceptions
- The parent tree in full: Exception hierarchy · Hinglish: 1.3.07 Exception hierarchy (Hinglish)
- Why inheritance makes "child caught by parent" work: 2.1.04-Inheritance
- Recording caught errors properly: 3.2.05-Logging