Exception Handling
Python uses “zero-cost” exception handling, which minimizes overhead when no exception occurs but efficiently handles exceptions when they are raised.Zero-Cost Exception Handling
The key idea:- No exception: Virtually no overhead (no explicit checks)
- Exception raised: Higher cost, but exceptions are rare
From Source to Bytecode
Consider this Python code:Pseudo-Instructions
SETUP_FINALLY and POP_BLOCK are pseudo-instructions:
- Appear in intermediate code
- Not actual bytecode instructions
SETUP_FINALLY: Specify exception handler locationPOP_BLOCK: Restore previous exception handler
The Exception Table
These pseudo-instructions are converted to an exception table stored inco_exceptiontable:
- Maps instruction offsets to exception handlers
- Consulted only when an exception occurs
- Instructions not covered by handlers don’t appear in the table
Handling Exceptions at Runtime
When an exception is raised:- Interpreter calls
get_exception_handler()in Python/ceval.c - Looks up current instruction offset in exception table
- If handler found: Transfer control to handler
- If not found: Bubble up to caller’s frame
- Repeat until handler found or topmost frame reached
Traceback Construction
During unwinding,PyTraceBack_Here() (Python/traceback.c) adds each frame to the traceback.
Exception Table Entries
Each entry contains:- Handler location - Offset of exception handler
- Stack depth - Stack depth at
trystatement - lasti flag - Whether to push instruction offset
Handler Execution Steps
- Pop values until stack depth matches handler’s depth
- If
lastiis true, push raising instruction offset - Push exception onto stack
- Jump to handler offset
Reraising Exceptions
Thelasti (last instruction) flag supports exception reraising:
lastipushed to stack during exception handlingRERAISEinstruction (withoparg > 0) sets instruction pointer tolasti- Traceback shows original raising location, not
finallyblock
Exception Table Format
Conceptually, the table is a sequence of 5-tuples:Design Goals
- Compact: Variable-sized entries for small offsets
- Searchable: Binary search in O(log n) time
Encoding Strategy
Store as(start, size, target, depth, push_lasti) instead of (start, end, ...):
- Size is always less than end offset
- More compact encoding
Varint Encoding
Uses 7-bit variable-length encoding:- First byte:
1Xdddddd(1 = start bit, X = extend bit, d = data) - Continuation bytes:
0Xdddddd - Extend bit set if another byte follows
Depth and Lasti Combined
Encoded together as(depth << 1) | lasti before encoding.
Example Entry
Code References
Constructing the Table
assemble_exception_table() in Python/assemble.c
Looking Up Handlers
get_exception_handler() in Python/ceval.c
Parsing in Python
Exception Chaining
Exception chaining sets__context__ and __cause__ attributes.
Implicit Chaining (__context__)
Set automatically by _PyErr_SetObject() in Python/errors.c:
PyErr_Set*() functions ultimately call _PyErr_SetObject().
Explicit Chaining (__cause__)
Set by RAISE_VARARGS bytecode:
Traceback Display
When displaying tracebacks:- Show original exception first
- If
__cause__is set: Print “The above exception was the direct cause…” - Else if
__context__is set and not suppressed: Print “During handling of the above exception…” - Show new exception
Example: Exception Table in Action
Performance Implications
Fast Path (No Exception)
- No runtime overhead from
tryblock - Same speed as code without exception handling
- Exception table consulted only if exception raised
Exception Path
- Table lookup: O(log n) binary search
- Stack unwinding: O(depth) to handler
- Traceback construction: O(depth) frame additions
Related Topics
- Bytecode Interpreter - How exceptions are raised
- Code Objects - Exception table storage
- Frames - Frame unwinding during exceptions
- Compiler Design - Exception table generation
