Skip to main content

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

FileRole
Python/errors.cPyErr_Set*, PyErr_Fetch, PyErr_NormalizeException.
Python/ceval.cThe unwind code path in the eval loop; exception-table lookup.
Python/assemble.cException-table encoding.
Objects/exceptions.cBaseException, ExceptionGroup, the standard hierarchy.
Python/traceback.cPyTraceBack_*, traceback printing.
Include/internal/pycore_pyerrors.hThread-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:

  1. Calls do_raise, which normalises the exception (constructs an instance if a class was raised, validates the value).
  2. Sets tstate->current_exception to the instance.
  3. 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 an except block; preserves the chain so the traceback shows "during handling of the above another exception occurred".
  • __cause__. Explicit. Set by raise X from Y.
  • __suppress_context__. Boolean; set by raise X from None to 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:

  1. Looks up the topmost _PyErr_StackItem.
  2. Sets tstate->current_exception to its value.
  3. 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. return and break inside finally raise SyntaxWarning at 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_note and __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.