The Hierarchy, raise from & finally
In short: every exception is a class, arranged in a family tree, and
except Cls: means "is this an instance of Cls, or any subclass of it?" —
so a clause catches a whole subtree, and the first clause that matches wins,
top to bottom. Exception is the parent of everything that's a program
error; BaseException also covers KeyboardInterrupt and SystemExit,
which is why except Exception: is the widest net you should ever cast.
Quarry adds its own branch under one base class so a caller can catch
"anything Quarry raised on purpose" without hiding real bugs, chains a
low-level error to the higher-level one it caused with raise New(...) from old, and uses finally to guarantee cleanup runs on every way out —
success, a handled error, or one that escapes.

Sort a KeyError from a renamed column
Quarry ran all night and reported success: zero errors, zero rows. Somebody had wrapped the loader like this:
try:
load_everything()
except Exception:
passA renamed column made every row raise KeyError. The except caught every
one, said nothing, and the run "succeeded" — a crash would have been better.
KeyError is a class, like every exception; __mro__ lists its ancestors,
nearest first:
Near the top, everything splits in two: BaseException is the root, and
directly under it sit exceptions that are not program errors —
KeyboardInterrupt, SystemExit. Exception is the parent of every error
you will ever raise or catch on purpose. A bare except: catches
BaseException too, so a program with one in its main loop can't even be
interrupted with Ctrl+C.
A clause catching a whole subtree has a consequence people trip over — order matters, because the first match wins:
row = {"id": "a1"}
try:
print(row["ms"])
except LookupError:
print("lookup")
except KeyError:
print("key")The right width is about what you'll do next, not which errors might happen:
catch narrowly where you can act (except ValueError: around int(text)),
and never catch just to silence — an exception you let go becomes a
traceback with a line number; one you swallow becomes a mystery. If one row
in a thousand is malformed, a batch of n rows is clean with probability
0.999 ** n — under 50% past about 700 rows, so a loader that aborts the
whole batch on one bad row fails most of its runs.
Give Quarry its own error branch
The builtin tree describes how something failed. It can't say what Quarry should do about it — so Quarry defines its own branch, under one base class, named for the response, not the symptom:
Three decisions are packed into those lines. One base class, so
except QuarryError: catches anything Quarry raised on purpose while
letting a genuine bug crash loudly. Names for the response:
SourceError means stop and retry later, BadRowError means skip this row
— the catching code doesn't need to know what went wrong inside. And
BadRowError is also a ValueError, via multiple inheritance, so code
written before Quarry had its own errors still catches it.
An exception class is an ordinary class, so give it the facts the handler will need instead of making it parse a message:
Chain a bad ms value into Quarry's vocabulary
The low-level ValueError from int is the true story, but it's the wrong
vocabulary for a caller who just wants to know "skip or stop?". The loader
catches it and raises its own error, saying where it came from:
raise NEW from OLD stores OLD on NEW.__cause__, and the traceback says
"the above exception was the direct cause". Raise inside an except without
from and Python remembers the handled exception anyway, as __context__,
worded "during handling of the above exception" — deliberately alarmed,
because it usually means the handler itself crashed. from None suppresses
a noisy original when only the new error's message matters:
try:
try:
{"yard": 1}["vult"]
except KeyError:
raise ValueError("unknown stage 'vult'") from None
except ValueError as err:
print(err.__cause__, err.__suppress_context__)Cleanup belongs in finally, which runs on every way out — success, a
handled error, an unhandled one, even a return:
That guarantee is why this batch loader is wrong — "close" only runs when
the loop finishes clean:
The batch is always supposed to be closed, even when a bad row makes it re-raise. Right now "close" is skipped on the failure path. Make the cleanup unconditional.
Traps worth knowing
A bare raise inside an except re-raises the exception being handled,
traceback intact — how you log-and-rethrow. A return inside finally
replaces whatever was happening, including an in-flight exception, which
vanishes without a trace — never do it. And an exception raised inside
finally replaces the one already in flight, chained with "during handling"
— keep finally bodies small and boring.
Quick practice
class QuarryError(Exception): pass
class BadRowError(QuarryError, ValueError): pass
def kind(exc):
if isinstance(exc, ValueError):
return "bad value"
if isinstance(exc, QuarryError):
return "quarry"
return "bug"
print(kind(BadRowError("x")))try:
try:
raise KeyError("stage")
except KeyError as exc:
raise ValueError("bad stage") from exc
except ValueError as err:
print(type(err.__cause__).__name__, err.__suppress_context__)close_file() is meant to run no matter what — right now a bad value skips it. Wrap the risky line so closing still happens.
🎯 Your turn
Build classify_error(stage, raw). The starter gives you Quarry's error
classes and its real loader, load_row(stage, raw), which can fail three
ways: a SourceError for an empty line, a BadRowError chained from a
ValueError for a malformed row, and a plain KeyError — a bug — when a
field is missing.
Call load_row. Return {"category": ..., "chain": [...]} where the
category is "ok", "bad-row", "source" or "bug", and the chain lists
the class names from the error you caught down to its root cause, following
__cause__ first, then __context__ unless it is suppressed.
classify_error("yard", "id=a1;ms=120") # → {"category": "ok", "chain": []}
classify_error("yard", "id=a2;ms=fast") # → {"category": "bad-row", "chain": ["BadRowError", "ValueError"]}
classify_error("vault", "") # → {"category": "source", "chain": ["SourceError"]}Press ▶ Run and hit ✓ Check. ✅
