Python 3.11 made exception handling “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.14, has three handlers.
def f(a):
try:
return a + 1
except KeyError:
return 0
00000000 82 08 0B 00 8B 0B 1A 03 99 01 1A 03
co_exceptiontable, 12 bytes| Bytes | Start | Length | Target | Depth | lasti | Byte offsets |
|---|---|---|---|---|---|---|
82 08 0B 00 | 2 | 8 | 11 | 0 | No | 4 to 20 → 22 |
8B 0B 1A 03 | 11 | 11 | 26 | 1 | Yes | 22 to 44 → 52 |
99 01 1A 03 | 25 | 1 | 26 | 1 | Yes | 50 to 52 → 52 |
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. 08 is a length of 8
units, ending at byte 20. 0B is target 11, byte 22. 00 gives
depth 0 and no lasti.
The bytes of the code are
80 00 1B 00 56 00 5E 01 2C 00 00 00 00 00 00 00 00 00 00 00
23 00 20 00 5C 00 00 00 00 00 00 00 00 00 06 00 64 05 00 00
1C 00 1F 00 1D 00 5E 00 23 00 69 00 3B 03 1D 00 69 01
and dis shows these three entries as:
ExceptionTable:
L1 to L2 -> L3 [0]
L3 to L4 -> L6 [1] lasti
L5 to L6 -> L6 [1] lasti
Here L1, L2 and L3 are offsets 4, 20 and 22, and L4, L5 and L6 are offsets 44, 50 and
52. The first entry covers the guarded return a + 1. 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
52.