Exceptions
An exception in Python is not a return code and not a special interpreter mode — it is an ordinary object, a subclass of BaseException. The raise statement marks such an object as the current exception and starts unwinding the stack, frame by frame, until it finds a frame whose try has a clause matching by isinstance. If nothing matched all the way to the top, control goes to sys.excepthook, which prints the traceback to sys.stderr, and the process exits with code 1. Since 3.11 the mechanism is "zero-cost" — the handler table lives beside the bytecode, and a try in which nothing was raised costs literally nothing at runtime. That is where the Python style of EAFP comes from — try first and catch the failure, rather than checking preconditions up front.
Python-specific behaviour starts immediately, and it is worth naming up front. The root of the hierarchy is BaseException, not Exception, and SystemExit, KeyboardInterrupt and GeneratorExit are deliberately kept outside Exception — so except Exception does not eat Ctrl+C, while a bare except: does. Clauses are checked top to bottom and only the first match runs, so a general base above a specific subclass turns the specific one into dead code — and the interpreter stays silent about it. The else block runs only on a clean pass, finally runs always — including the case where a return inside finally quietly discards the in-flight exception. And a bare raise is the only form of re-raising that does not append frames to the traceback which never took part in the failure. Work through each mechanism in the layers below.
Topic map
- Handling and raise — how
raiseunwinds the stack, what may be raised at all, and where an uncaught exception ends up. - The BaseException hierarchy — why
SystemExit,KeyboardInterruptandGeneratorExitlive outsideException, and when aSyntaxErroris catchable after all. - except clause order — the first-match rule, subclasses before bases, and how
except*inverts it. - The else block — code that runs only when nothing was raised, and why that differs from putting it inside
try. - The finally block — guaranteed cleanup, and the trap where a
returninsidefinallyerases the exception. - Re-raising and chaining — a bare
raiseversusraise e, implicit__context__and explicit__cause__viaraise ... from. - Custom exceptions — an application-owned error hierarchy, payload in attributes, and
args. - Warnings — the
warningsmodule as the non-fatal channel, and its once-per-location default filter.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
Writing a bare except: instead of except Exception | The clause catches all of BaseException — Ctrl+C and sys.exit() stop working, and the process becomes unkillable by normal means |
Placing except Exception above except ValueError | The specific clause is unreachable, yet CPython emits neither an error nor a warning — only a linter sees the dead code |
| Expecting every matching clause to run | Exactly one runs — the first match from the top; the rest are skipped even when they are more precise |
Thinking else runs when an exception occurred | The opposite — else runs only on a clean pass through try, and always before finally |
Putting return, break or continue inside finally | The in-flight exception vanishes without a trace and the try return value is replaced — Python 3.14 flags this with a SyntaxWarning |
Writing raise e instead of a bare raise | The handler's frame is appended to the traceback, so the chain stops pointing at the real site of the failure |
Deriving a custom exception from BaseException | Every existing except Exception stops catching it, and an application error takes the process down on a par with Ctrl+C |
Assuming warnings.warn halts execution | The warning goes through the filter and is printed to stderr while the code continues; it only becomes an exception under the error filter |
What interviews check
The topic is part of the mandatory minimum and is probed in two passes. The first is about the machinery — you are asked to sketch the top of the hierarchy and explain why except Exception does not catch KeyboardInterrupt. The right answer separates errors from control signals — SystemExit, KeyboardInterrupt and GeneratorExit are kept outside Exception precisely so that "catch every error" never means "catch the order to shut down". A question about the bare except: almost always follows — call it the equivalent of except BaseException and explain why that is dangerous in a long-running service.
The second pass is code on a screen. The classic is a snippet with except Exception above except ValueError and a request to explain why the second clause is unreachable; here you must name the first-match rule rather than "Python picks the most precise handler". Next comes a try/finally with a return inside finally and the question of what the function returns and where the exception went. Then an except chain with raise e and a question about the traceback, where the interviewer expects a bare raise and the difference between __context__ and __cause__. The typical failure across all of these is the same — the candidate remembers the syntax but not the semantics of order and evaluation time, so say it out loud: clauses are checked top to bottom, else only on a clean pass, finally always, and an argument-less raise spoils nothing.