Exceptions
CPython exceptions cost nothing on the success path. There is no
SETUP_FINALLY opcode, no per-try push; entering a try block
emits no instructions at all. Instead the compiler emits an
exception table: a side table on the code object that maps
bytecode-offset ranges to handler offsets. When an exception is
raised, the runtime walks the frame chain, consulting each
frame's exception table to find a handler. Raising is slow;
not raising is free.
Where the code lives
| File | Role |
|---|---|
Python/errors.c | PyErr_Set*, PyErr_Fetch, PyErr_NormalizeException. |
Python/ceval.c | The unwind code path in the eval loop; exception-table lookup. |
Python/assemble.c | Exception-table encoding. |
Objects/exceptions.c | BaseException, ExceptionGroup, the standard hierarchy. |
Python/traceback.c | PyTraceBack_*, traceback printing. |
Include/internal/pycore_pyerrors.h | Thread-state exception fields. |
The exception table
co_exceptiontable: sequence of variable-length records
each record: (start_offset, end_offset, target_offset, depth, lasti)
A try: ... except: H compiles to:
[try body bytecode] # offsets [start, end)
JUMP past_handler
H: # target_offset
[handler bytecode]
past_handler:
Plus an entry (start, end, H, depth_at_H, lasti) in the
exception table. The eval loop never executes a setup opcode;
the try region is just plain bytecode. On a raise, the unwinder
linearly scans the table for the deepest entry containing the
current instr_ptr and jumps to its target_offset with the
value stack truncated to depth.
The encoding is variable-length integers tuned for compact
representation; the format is documented in Objects/exception_handling_notes.txt.
Raise
raise X compiles to RAISE_VARARGS. The opcode body:
- Calls
do_raise, which normalises the exception (constructs an instance if a class was raised, validates the value). - Sets
tstate->current_exceptionto the instance. - Falls through to the exception-unwind path in the eval loop.
The unwind path:
/* Python/ceval.c (sketch) */
exception_unwind:
handler = _PyCode_LookupException(co, instr_ptr - first_instr);
if (handler == NULL) {
/* no handler in this frame: pop frame, retry in caller */
propagate_to_caller();
}
/* truncate stack to handler.depth; push the exception; jump */
The lookup is O(log N) over the table (the table is sorted by range start) or O(N) for small tables. The cost is paid only on raise; the success path never touches it.
Exception state on the thread
/* Include/internal/pycore_pyerrors.h _PyErr_StackItem */
typedef struct _err_stackitem {
PyObject *exc_value; /* current exception value */
struct _err_stackitem *previous_item;
} _PyErr_StackItem;
Each frame that catches an exception pushes a new _PyErr_StackItem
so the catching code can re-raise the original via raise. The
chain is also the basis of implicit chaining: a new exception
raised while another is being handled has its __context__
automatically set to the previous one.
Chaining
Three kinds:
__context__. Implicit. Set when a new exception is raised inside anexceptblock; preserves the chain so the traceback shows "during handling of the above another exception occurred".__cause__. Explicit. Set byraise X from Y.__suppress_context__. Boolean; set byraise X from Noneto hide the implicit__context__chain.
The opcode RAISE_VARARGS and its helpers in Python/errors.c
manage these fields; the traceback printer in
Python/traceback.c walks them.
ExceptionGroup (PEP 654)
PEP 654 added a group exception type and the except* syntax:
try:
...
except* TypeError as eg:
... # eg is an ExceptionGroup of TypeErrors
except* ValueError as eg:
...
except* compiles into a different shape than except: the
handler receives a split of the group matching its type, and
the unmatched part propagates. The matching uses
BaseExceptionGroup.split:
/* Objects/exceptions.c */
static PyObject *exceptiongroup_split(PyObject *self, PyObject *match);
split(matcher) returns a (matched, rest) pair; the eval loop
constructs the handler's view and re-raises rest if non-None.
The group hierarchy is:
BaseException
BaseExceptionGroup
ExceptionGroup (when all members are Exception subclasses)
BaseExceptionGroup.__new__ switches the returned class to
ExceptionGroup automatically when all members qualify, so most
user code only ever sees ExceptionGroup.
Tracebacks (PEP 657)
A traceback is a linked list of PyTracebackObject:
/* Include/cpython/traceback.h PyTracebackObject */
typedef struct _traceback {
PyObject_HEAD
struct _traceback *tb_next;
PyFrameObject *tb_frame;
int tb_lasti; /* bytecode offset where exception occurred */
int tb_lineno;
} PyTracebackObject;
Each entry remembers the frame and the bytecode offset. PEP 657
made the display of tracebacks fine-grained: instead of just
the line, the printer reads the (col_offset, end_col_offset)
for tb_lasti from the code object's location table and
underlines the exact expression:
File "x.py", line 3, in <module>
return foo() + bar()
~~~~~^
The rendering lives in Lib/traceback.py; the data is in the
location table (co_linetable, see compile).
Bare raise
raise with no operand re-raises the currently active
exception. It compiles to RERAISE, which:
- Looks up the topmost
_PyErr_StackItem. - Sets
tstate->current_exceptionto its value. - Enters the unwind path.
If no exception is active, RERAISE raises RuntimeError("No active exception to re-raise").
try-finally
finally compiles into two copies of its body: one in the
success path (executed before falling off the try), one as a
handler entry (executed before propagating). The handler entry
re-raises after the body runs. PEP 765 warns at compile time
if a return or break inside finally would silently
discard a pending exception.
CPython 3.14 changes
- PEP 765 warnings.
returnandbreakinsidefinallyraiseSyntaxWarningat compile time because they silently cancel any propagating exception, which is almost always a bug. - Exception group display. Group rendering picked up the PEP 657 underlines for the cause expression of each leaf exception.
PEP touchpoints
- PEP 654. Exception groups and
except*. - PEP 657. Fine-grained error locations in tracebacks.
- PEP 678.
add_noteand__notes__on exceptions. - PEP 765. Disallow control flow in
finally.
Reference
Python/errors.c,Python/ceval.c,Python/assemble.c,Objects/exceptions.c,Python/traceback.c.Objects/exception_handling_notes.txt. Exception-table format.- PEP 654, PEP 657, PEP 678, PEP 765.