Exception handling in Python 3.13 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.13, 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 0A 14 03 93 01 14 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 0A 14 03 | 7 | 10 | 20 | 1 | Yes | 14 to 34 → 40 |
93 01 14 03 | 19 | 1 | 20 | 1 | Yes | 38 to 40 → 40 |
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
95 00 1E 00 55 00 53 01 2D 00 00 00 24 00 21 00 5B 00 00 00
00 00 00 00 00 00 07 00 61 03 00 00 20 00 1F 00 67 02 66 00
3D 03 1F 00 66 01
which Python 3.13’s dis, asked to show offsets, prints as:
1 0 RESUME 0
2 2 NOP
3 L1: 4 LOAD_FAST 0 (a)
6 LOAD_CONST 1 (1)
8 BINARY_OP 0 (+)
L2: 12 RETURN_VALUE
-- L3: 14 PUSH_EXC_INFO
4 16 LOAD_GLOBAL 0 (KeyError)
26 CHECK_EXC_MATCH
28 POP_JUMP_IF_FALSE 3 (to L5)
32 POP_TOP
5 L4: 34 POP_EXCEPT
36 RETURN_CONST 2 (0)
4 L5: 38 RERAISE 0
-- L6: 40 COPY 3
42 POP_EXCEPT
44 RERAISE 1
ExceptionTable:
L1 to L2 -> L3 [0]
L3 to L4 -> L6 [1] lasti
L5 to L6 -> L6 [1] lasti
Python 3.13’s dis names offsets with labels, and an entry runs from its
first label up to, but not including, the second. 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
40.