Trying to understand the bytecode changes in Python 3.10

In Python 3.9

>>> def test_assertion(tracer):
...     tracer.start()
...     try:
...         assert False
...     except AssertionError:
...         pass
...     tracer.stop()
>>> dis(test_assertion)
  2           0 LOAD_FAST                0 (tracer)
              2 LOAD_METHOD              0 (start)
              4 CALL_METHOD              0
              6 POP_TOP

  3           8 SETUP_FINALLY           12 (to 22)

  4          10 LOAD_CONST               1 (False)
             12 POP_JUMP_IF_TRUE        18
             14 LOAD_ASSERTION_ERROR
             16 RAISE_VARARGS            1
        >>   18 POP_BLOCK
             20 JUMP_FORWARD            18 (to 40)

  5     >>   22 DUP_TOP
             24 LOAD_GLOBAL              1 (AssertionError)
             26 JUMP_IF_NOT_EXC_MATCH    38
             28 POP_TOP
             30 POP_TOP
             32 POP_TOP

  6          34 POP_EXCEPT
             36 JUMP_FORWARD             2 (to 40)
        >>   38 RERAISE

  7     >>   40 LOAD_FAST                0 (tracer)
             42 LOAD_METHOD              2 (stop)
             44 CALL_METHOD              0
             46 POP_TOP
             48 LOAD_CONST               0 (None)
             50 RETURN_VALUE

Same code in 3.10

>>> def test_assertion(tracer):
...     tracer.start()
...     try:
...         assert False
...     except AssertionError:
...         pass
...     tracer.stop()
>>> dis(test_assertion)
  2           0 LOAD_FAST                0 (tracer)
              2 LOAD_METHOD              0 (start)
              4 CALL_METHOD              0
              6 POP_TOP

  3           8 SETUP_FINALLY            2 (to 14)

  4          10 LOAD_ASSERTION_ERROR
             12 RAISE_VARARGS            1

  5     >>   14 DUP_TOP
             16 LOAD_GLOBAL              1 (AssertionError)
             18 JUMP_IF_NOT_EXC_MATCH    15 (to 30)
             20 POP_TOP
             22 POP_TOP
             24 POP_TOP

  6          26 POP_EXCEPT
             28 JUMP_FORWARD             1 (to 32)

  5     >>   30 RERAISE                  0

  7     >>   32 LOAD_FAST                0 (tracer)
             34 LOAD_METHOD              2 (stop)
             36 CALL_METHOD              0
             38 POP_TOP
             40 LOAD_CONST               0 (None)
             42 RETURN_VALUE

I see some changes in bytecode

  • The LOAD_CONST and POP_JUMP_IF_TRUE before LOAD_ASSERTION_ERROR is gone
  • POP_BLOCK and JUMP_FORWARD after RAISE_VARARGS also gone

Can someone help point to the commits and/or bpos that caused these changes?

You can see the history of all bytecode changes by looking at the commits changing the PYC magic number, but it doesn’t look like these are actually bytecode changes, it looks like the compiler got smarter at dead code elimination. The POP_JUMP_IF_TRUE is always going to fail, so that can be eliminated along with its constant load. That’s the only thing jumping to the POP_BLOCK, so that is then eliminated too. I think bpo-42615 and bpo-42908 might be involved?

Note also, in 3.11 exception handling was changed entirely. Using a seperate lookup table which records bytecode ranges and the location of the relevant exception handler instructions, a lot of the instructions can be removed.

1 Like

Thanks! Also the trick of looking at the magic number is really helpful.