Exercises — Exception hierarchy
Before we start, one picture of the slice of the tree we will keep using — pin it in your head.

Read it as: an arrow means "is a kind of". KeyError is a kind of LookupError, which is a kind of Exception, which is a kind of BaseException. A handler for a box catches that box and everything below it. Notice the whole left branch under BaseException — KeyboardInterrupt, SystemExit, GeneratorExit — sits outside Exception on purpose; that placement is the punchline of half these problems.
Level 1 — Recognition
Recall Solution L1.1
Walk up one step in the figure.
- (a)
ZeroDivisionError→ parent is ==ArithmeticError==. - (b)
IndexError→ parent is ==LookupError==. - (c)
FileNotFoundError→ parent is ==OSError==. - (d)
KeyboardInterrupt→ parent is ==BaseException== directly (NOTException). This is the key one: Ctrl+C lives outside theExceptionbranch on purpose, soexcept Exceptionnever swallows it.
Recall Solution L1.2
Rule: a handler catches a box only if the raised exception is that box or a descendant of it (arrow-path leading up to it).
- (a) Yes —
KeyErroris belowLookupError. - (b) No —
IndexErrorandKeyErrorare siblings; neither is below the other. - (c) No —
SystemExitsits underBaseExceptionbut not underException. - (d) Yes —
ZeroDivisionError→ArithmeticError→Exception; the path reachesException.
Level 2 — Application
Recall Solution L2.1
Trace the two calls, then remember each outer print prints whatever f returned.
f(10, 2): no exception,freturns5.0(a float —/always makes a float).fprints nothing itself. The outerprint(f(10, 2))then prints5.0.f(10, 0): dividing by zero raisesZeroDivisionError. Handlers are checked top→bottom; the first,except ZeroDivisionError, matches, so insidefit printsAand returnsNone. The outerprint(f(10, 0))then printsNone.
So there are three screen lines total — one from inside f (A) and one from each of the two outer prints (5.0, None):
5.0
A
None
(The order is: outer print of call 1 → the A printed inside call 2 → outer print of call 2.)
Recall Solution L2.2
Both IndexError and KeyError sit under LookupError, so one parent handler covers both:
try:
item = data[index]
value = item[key]
except LookupError:
print("lookup failed")This is the 80/20 move from Example 3 in the parent: use the nearest common ancestor when you don't need different recovery logic for each child.
Recall Solution L2.3
List the classes in a parenthesised tuple after except:
try:
item = data[index]
value = item[key]
except (KeyError, IndexError) as e:
print(f"lookup failed: {e}")The parentheses are required — except KeyError, IndexError: is a syntax error in Python 3. This handler catches an exception if it is a descendant of any class in the tuple.
When to prefer the tuple over except LookupError: when you want to catch a specific pair of siblings that live under a broad parent whose other children you deliberately want to let through. except LookupError would also grab those other children; the tuple names only the two you chose. It's also handy for pulling together cousins from different branches, e.g. except (ValueError, KeyError):, which have no useful common ancestor short of Exception.
Level 3 — Analysis
Recall Solution L3.1
FileNotFoundError and PermissionError are both children of OSError. Since except OSError is written first and it catches its own descendants, any FileNotFoundError or PermissionError is grabbed at line (1).
- Reachable: (1) only.
- Dead code: (2) and (3) — never reached. Fix ordering: put the specific children first:
except FileNotFoundError: ...
except PermissionError: ...
except OSError: ... # safety net lastRecall Solution L3.2
Ctrl+C raises KeyboardInterrupt. In the tree, KeyboardInterrupt hangs directly off BaseException, on a different branch from Exception (the same branch as SystemExit and GeneratorExit). A handler catches only its box and descendants, and KeyboardInterrupt is not a descendant of Exception. So the except Exception never matches it, the exception keeps propagating up to BaseException, finds no handler, and terminates the program — exactly the safe behaviour we want.
Level 4 — Synthesis
Recall Solution L4.1
def read_config(path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError: # specific child first
return "DEFAULT"
except OSError: # parent safety net (still under Exception,
return None # so KeyboardInterrupt is untouched)Why this satisfies (4): neither FileNotFoundError nor OSError is an ancestor of KeyboardInterrupt, so Ctrl+C passes straight through. We never wrote except BaseException, which would have been the only way to accidentally trap it. See 1.3.09-Context-Managers for why with open(...) also closes the file safely on error.
Recall Solution L4.2
Order every handler so no ancestor blocks a needed descendant:
try:
work()
except ZeroDivisionError: # child of ArithmeticError
print("zero")
except ArithmeticError: # parent — catches OverflowError etc.
print("math")
except LookupError: # covers IndexError + KeyError
print("lookup")
except Exception: # everything else, but not SystemExit/Ctrl+C
print("other")Check the tree: ZeroDivisionError before its parent ArithmeticError ✓; LookupError handles both sibling children ✓; Exception last as the broad net that still excludes the BaseException-only branch ✓.
Level 5 — Mastery
Recall Solution L5.1
Inherit from Exception:
class ConfigError(Exception):
passWhy: subclassing Exception (not BaseException) drops ConfigError onto the normal-errors branch. Now except Exception catches it, except ConfigError catches it specifically, and it stays off the system-signal branch — so it never interferes with SystemExit/KeyboardInterrupt. This is the standard rule; full treatment in 1.3.08-Custom-Exceptions, and the inheritance mechanics come from 2.1.04-Inheritance.
Recall Solution L5.2
The key fact students get wrong: Python's built-in float arithmetic operators (+ - * /) do NOT raise OverflowError — they return the special float inf on overflow. OverflowError comes from places that refuse to produce inf, such as many math functions and conversions.
- (a)
1e308 / 1e-10: the true value exceeds the largest finite float (). The/operator does not raise — it returnsfloat('inf'). - (b)
10**1000 / 10**999:10**1000and10**999are arbitrary-precision integers; their exact ratio is , which fits easily, so/returns the float10.0— no overflow at all. - (c)
math.exp(1000):math.expwould need , far past the float range, and unlike/it raisesOverflowErrorrather than returninginf. Lesson: overflow behaviour depends on who does the arithmetic. Plain float operators →inf; certainmath/conversion routines →OverflowError; unbounded Pythonints → never overflow.
Recall Self-test: fill the reveal
A handler catches an exception when the exception is that class or a ::: descendant of it (following arrows upward)
Order handlers from ::: specific (children) to general (parents), top to bottom
except Exception deliberately does NOT catch ::: KeyboardInterrupt, SystemExit and GeneratorExit (they sit under BaseException only)
To catch two specific siblings in one line without naming their parent, write ::: except (KeyError, IndexError)
1e308 / 1e-10 evaluates to ::: inf (the float / operator returns inf, it does not raise OverflowError)
10**1000 / 10**999 returns ::: 10.0 (no overflow — Python ints are unbounded)
A custom app error should subclass ::: Exception (not BaseException)