What is the `ExceptionTable` in the output of `dis`?
Question:
In python3.13
, when I try to disassemble [i for i in range(10)]
, the result is as below:
>>> import dis
>>>
>>> dis.dis('[i for i in range(10)]')
0 RESUME 0
1 LOAD_NAME 0 (range)
PUSH_NULL
LOAD_CONST 0 (10)
CALL 1
GET_ITER
LOAD_FAST_AND_CLEAR 0 (i)
SWAP 2
L1: BUILD_LIST 0
SWAP 2
L2: FOR_ITER 4 (to L3)
STORE_FAST_LOAD_FAST 0 (i, i)
LIST_APPEND 2
JUMP_BACKWARD 6 (to L2)
L3: END_FOR
L4: SWAP 2
STORE_FAST 0 (i)
RETURN_VALUE
-- L5: SWAP 2
POP_TOP
1 SWAP 2
STORE_FAST 0 (i)
RERAISE 0
ExceptionTable:
L1 to L4 -> L5 [2]
At the end of the output, there’s something ExceptionTable
. It does not exist in the previous versions of python.
Python 3.10.0b1 (default, May 4 2021, 00:00:00) [GCC 10.2.1 20201125 (Red Hat 10.2.1-9)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>>
>>> dis.dis('[i for i in range(10)]')
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x7f3d412503a0, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (range)
8 LOAD_CONST 2 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x7f3d412503a0, file "<dis>", line 1>:
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 4 (to 14)
6 STORE_FAST 1 (i)
8 LOAD_FAST 1 (i)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 2 (to 4)
>> 14 RETURN_VALUE
I can’t understand what that means, also I couldn’t find any document for this.
Answers:
ExceptionTable
determines where to jump to when an exception is raised(it was implemented in python-3.11). Prior version uses separate opcodes to handle this. The advantage of this approach is that entering and leaving a try block normally does not execute any code, making execution faster.
To access this table, you can do so by accessing the co_exceptiontable
attribute of the code object. The ExceptionTable
representation returned by dis
is the result of parsing this table.
>>> def foo():
... c = 1 + 2
... return c
...
>>>
>>> foo.__code__.co_exceptiontable
b''
>>> def foo():
... try:
... 1/0
... except:
... pass
...
>>> foo.__code__.co_exceptiontable
b'x82x05x08x00x88x02x0cx03'
>>>
>>> from dis import _parse_exception_table
>>>
>>> _parse_exception_table(foo.__code__)
[_ExceptionTableEntry(start=4, end=14, target=16, depth=0, lasti=False), _ExceptionTableEntry(start=16, end=20, target=24, depth=1, lasti=True)]
A detailed explanation about ExceptionTable
is available here. Quoting from the same link:
python-3.11 uses what is known as "zero-cost" exception handling.
Prior to python-3.11, exceptions were handled by a runtime stack of "blocks".
In zero-cost exception handling, the cost of supporting exceptions is
minimized. In the common case (where no exception is raised) the cost
is reduced to zero (or close to zero). The cost of raising an
exception is increased, but not by much.
The following code:
def f():
try:
g(0)
except:
return "fail"
compiles as follows in 3.10:
2 0 SETUP_FINALLY 7 (to 16)
3 2 LOAD_GLOBAL 0 (g)
4 LOAD_CONST 1 (0)
6 CALL_NO_KW 1
8 POP_TOP
10 POP_BLOCK
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
4 >> 16 POP_TOP
18 POP_TOP
20 POP_TOP
5 22 POP_EXCEPT
24 LOAD_CONST 3 ('fail')
26 RETURN_VALUE
Note the explicit instructions to push and pop from the "block" stack:
SETUP_FINALLY
and POP_BLOCK
.
In 3.11, the SETUP_FINALLY
and POP_BLOCK
are eliminated, replaced with
a table to determine where to jump to when an exception is raised.
1 0 RESUME 0
2 2 NOP
3 4 LOAD_GLOBAL 1 (g + NULL)
16 LOAD_CONST 1 (0)
18 PRECALL 1
22 CALL 1
32 POP_TOP
34 LOAD_CONST 0 (None)
36 RETURN_VALUE
>> 38 PUSH_EXC_INFO
4 40 POP_TOP
5 42 POP_EXCEPT
44 LOAD_CONST 2 ('fail')
46 RETURN_VALUE
>> 48 COPY 3
50 POP_EXCEPT
52 RERAISE 1
ExceptionTable:
4 to 32 -> 38 [0]
38 to 40 -> 48 [1] lasti
(Note this code is from python-3.11, later versions may have slightly
different bytecode.)
If an instruction raises an exception then its offset is used to find
the target to jump to. For example, the CALL
at offset 22
, falls into
the range 4
to 32
. So, if g()
raises an exception, then control jumps
to offset 38
.
In python3.13
, when I try to disassemble [i for i in range(10)]
, the result is as below:
>>> import dis
>>>
>>> dis.dis('[i for i in range(10)]')
0 RESUME 0
1 LOAD_NAME 0 (range)
PUSH_NULL
LOAD_CONST 0 (10)
CALL 1
GET_ITER
LOAD_FAST_AND_CLEAR 0 (i)
SWAP 2
L1: BUILD_LIST 0
SWAP 2
L2: FOR_ITER 4 (to L3)
STORE_FAST_LOAD_FAST 0 (i, i)
LIST_APPEND 2
JUMP_BACKWARD 6 (to L2)
L3: END_FOR
L4: SWAP 2
STORE_FAST 0 (i)
RETURN_VALUE
-- L5: SWAP 2
POP_TOP
1 SWAP 2
STORE_FAST 0 (i)
RERAISE 0
ExceptionTable:
L1 to L4 -> L5 [2]
At the end of the output, there’s something ExceptionTable
. It does not exist in the previous versions of python.
Python 3.10.0b1 (default, May 4 2021, 00:00:00) [GCC 10.2.1 20201125 (Red Hat 10.2.1-9)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>>
>>> dis.dis('[i for i in range(10)]')
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x7f3d412503a0, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (range)
8 LOAD_CONST 2 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x7f3d412503a0, file "<dis>", line 1>:
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 4 (to 14)
6 STORE_FAST 1 (i)
8 LOAD_FAST 1 (i)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 2 (to 4)
>> 14 RETURN_VALUE
I can’t understand what that means, also I couldn’t find any document for this.
ExceptionTable
determines where to jump to when an exception is raised(it was implemented in python-3.11). Prior version uses separate opcodes to handle this. The advantage of this approach is that entering and leaving a try block normally does not execute any code, making execution faster.
To access this table, you can do so by accessing the co_exceptiontable
attribute of the code object. The ExceptionTable
representation returned by dis
is the result of parsing this table.
>>> def foo():
... c = 1 + 2
... return c
...
>>>
>>> foo.__code__.co_exceptiontable
b''
>>> def foo():
... try:
... 1/0
... except:
... pass
...
>>> foo.__code__.co_exceptiontable
b'x82x05x08x00x88x02x0cx03'
>>>
>>> from dis import _parse_exception_table
>>>
>>> _parse_exception_table(foo.__code__)
[_ExceptionTableEntry(start=4, end=14, target=16, depth=0, lasti=False), _ExceptionTableEntry(start=16, end=20, target=24, depth=1, lasti=True)]
A detailed explanation about ExceptionTable
is available here. Quoting from the same link:
python-3.11 uses what is known as "zero-cost" exception handling.
Prior to python-3.11, exceptions were handled by a runtime stack of "blocks".In zero-cost exception handling, the cost of supporting exceptions is
minimized. In the common case (where no exception is raised) the cost
is reduced to zero (or close to zero). The cost of raising an
exception is increased, but not by much.The following code:
def f(): try: g(0) except: return "fail" compiles as follows in 3.10: 2 0 SETUP_FINALLY 7 (to 16) 3 2 LOAD_GLOBAL 0 (g) 4 LOAD_CONST 1 (0) 6 CALL_NO_KW 1 8 POP_TOP 10 POP_BLOCK 12 LOAD_CONST 0 (None) 14 RETURN_VALUE 4 >> 16 POP_TOP 18 POP_TOP 20 POP_TOP 5 22 POP_EXCEPT 24 LOAD_CONST 3 ('fail') 26 RETURN_VALUE
Note the explicit instructions to push and pop from the "block" stack:
SETUP_FINALLY
andPOP_BLOCK
.In 3.11, the
SETUP_FINALLY
andPOP_BLOCK
are eliminated, replaced with
a table to determine where to jump to when an exception is raised.1 0 RESUME 0 2 2 NOP 3 4 LOAD_GLOBAL 1 (g + NULL) 16 LOAD_CONST 1 (0) 18 PRECALL 1 22 CALL 1 32 POP_TOP 34 LOAD_CONST 0 (None) 36 RETURN_VALUE >> 38 PUSH_EXC_INFO 4 40 POP_TOP 5 42 POP_EXCEPT 44 LOAD_CONST 2 ('fail') 46 RETURN_VALUE >> 48 COPY 3 50 POP_EXCEPT 52 RERAISE 1 ExceptionTable: 4 to 32 -> 38 [0] 38 to 40 -> 48 [1] lasti
(Note this code is from python-3.11, later versions may have slightly
different bytecode.)If an instruction raises an exception then its offset is used to find
the target to jump to. For example, theCALL
at offset22
, falls into
the range4
to32
. So, ifg()
raises an exception, then control jumps
to offset38
.