Python's error handling looks like Java's try/catch, with two twists: the
culture is EAFP — "easier to ask forgiveness than permission," i.e. try the operation and handle
failure rather than checking first — and the block has four parts whose ordering is worth getting exactly
right: try, except, else, finally.
try — the risky code. Run it and watch for exceptions.except SomeError — the handler. Runs only if the matching exception was
raised. You can have several, checked top-to-bottom; the first matching type wins.else — runs only if the try finished with no exception. It's
where you put "the rest of the happy path," kept out of the try so you don't accidentally
catch its errors.finally — runs no matter what: success, handled error, or even an error on
its way out the door. This is your cleanup (close a file, release a lock).Set whether the try body raises, and which error, then trace exactly which blocks run:
The single most common mistake is a bare except: that swallows every error —
including KeyboardInterrupt — or an except Exception: that swallows every
bug (typos included) and hides it.
Catch the specific exception you know how to handle and let the rest propagate. Propagation is a
feature: an unhandled exception travels up the call stack until something catches it, and if nothing
does, Python prints a traceback pinpointing where it came from.
When you need your own error category, subclass Exception:
class InvalidRowError(Exception): pass — then raise InvalidRowError("bad line 7")
and callers can except InvalidRowError precisely. This is exactly the pattern your
Phase 0 capstone uses to flag bad CSV rows.
When a handler itself turns one error into another — a parse failure becoming your own
ParseError, say — write raise NewError(...) from e rather than a bare
raise NewError(...). Both produce a traceback, but only from e records why:
Python prints the original error, then the sentence "the above exception was the direct cause of the
following exception," and the new error's __cause__ attribute holds e for any code
that wants to inspect it. Losing that chain is losing the reason a bug happened, not just the fact that it
did.
The four-block trace above already showed which blocks run when the try body raises. The
question this section adds: what if the except handler itself raises — while cleaning up,
while re-wrapping the error, anything? finally doesn't care whose exception is leaving; it still
runs on the way out, every time, even when that exception was born inside the handler and not the
try.
Since Python 3.11, ExceptionGroup lets code raise several unrelated
errors at once (the classic case: a batch of independent tasks, more than one of which failed) and
except* — note the star — matches against the errors inside the group by type, so more
than one except* clause can fire for a single raise, one per matching sub-exception.
And the title's last clause — what never to catch. "Catch narrowly" above said not to write
except Exception: pass; here is why it doesn't even buy the safety it looks like:
KeyboardInterrupt and SystemExit inherit directly from BaseException,
not Exception, so Ctrl-C and sys.exit() still get through — while every
real bug in the block (a typo, a wrong type) is gone with no trace, a quietly wrong answer instead of
a traceback pointing at the line.
try runs risky code; the first matching
except handles it; else runs only on success; finally runs
always (cleanup). Catch specific exceptions — never a bare except. Uncaught
exceptions propagate up the stack and print a traceback. Define domain errors by subclassing
Exception.Guaranteed cleanup in finally is usually better expressed with a
with block →