Exception handling in Python 3.12 is “zero cost”: no instruction sets up a handler on
entry to a try block. Instead, co_exceptiontable lists which
instructions are covered and where each goes when an exception is raised. If the
interpreter is running an instruction not in any entry, the exception leaves the
frame.
Entries
Each entry is four numbers, written one after the other:
| Number | Meaning |
|---|---|
| Start | The first covered code unit |
| Length | How many code units are covered |
| Target | The code unit of the handler to jump to |
| Depth and lasti | (depth << 1) | lasti |
The first three are in 16-bit code units; multiply by 2 for a byte offset in
co_code. Depth is the value-stack depth to unwind to before the
handler runs. When lasti is 1, the offset of the instruction that raised is
pushed first, so a later RERAISE can restore it. Entries are sorted by
start, and an instruction is covered by at most one entry.
Varint encoding
Each number is a big-endian varint: the highest bits come first, with 6 bits a byte. Bit 6 means another byte follows. Bit 7 is set only on the first byte of an entry, so a reader can find entries by their first byte, and it is clear on every other byte.
SBit 7: set on the first byte of an entry’s first number1 bitXBit 6: another byte follows, and extends this number1 bitdBits 0–5: data, highest first6 bits
The value is built by adding the data of each byte in turn, multiplying what you have so far by 64 before each next byte.
Worked example
This function, compiled under Python 3.12, has an exception table of three entries, which lead to two handlers.
def f(a):
try:
return a + 1
except KeyError:
return 0
00000000 82 04 07 00 87 09 13 03 92 01 13 03
co_exceptiontable, 12 bytes| Bytes | Start | Length | Target | Depth | lasti | Byte offsets |
|---|---|---|---|---|---|---|
82 04 07 00 | 2 | 4 | 7 | 0 | No | 4 to 12 → 14 |
87 09 13 03 | 7 | 9 | 19 | 1 | Yes | 14 to 32 → 38 |
92 01 13 03 | 18 | 1 | 19 | 1 | Yes | 36 to 38 → 38 |
Reading the first entry: 82 has bit 7 set, the start of an entry, and
data 2, so the start is code unit 2, byte offset 4. 04 is a length of 4
units, ending at byte 12. 07 is target 7, byte 14. 00 gives
depth 0 and no lasti.
The bytes of the code are
97 00 09 00 7C 00 64 01 7A 00 00 00 53 00 23 00 74 00 00 00
00 00 00 00 00 00 24 00 72 03 01 00 59 00 79 02 77 00 78 03
59 00 77 01
which Python 3.12’s dis shows as:
0 RESUME 0
2 NOP
4 LOAD_FAST 0 (a)
6 LOAD_CONST 1 (1)
8 BINARY_OP 0 (+)
12 RETURN_VALUE
14 PUSH_EXC_INFO
16 LOAD_GLOBAL 0 (KeyError)
26 CHECK_EXC_MATCH
28 POP_JUMP_IF_FALSE 3 (to 36)
30 POP_TOP
32 POP_EXCEPT
34 RETURN_CONST 2 (0)
36 RERAISE 0
38 COPY 3
40 POP_EXCEPT
42 RERAISE 1
ExceptionTable:
4 to 10 -> 14 [0]
14 to 30 -> 38 [1] lasti
36 to 36 -> 38 [1] lasti
Python 3.12’s dis prints the offset of the last covered instruction,
not the end of the range. The first entry covers the guarded return a + 1,
without its RETURN_VALUE. The other two cover the handler: the code that
compares the exception with KeyError and the RERAISE, sending
anything that goes wrong to the cleanup code at offset 38.