pythonpython-2.7bytecodecpython

What is the purpose of END_FINALLY bytecode in older Python versions?


Consider the following code:

try:
    helloworld()
except:
    failure()

and its disassembly (this is in Python 2.7):

  1           0 SETUP_EXCEPT            11 (to 14)

  2           3 LOAD_NAME                0 (helloworld)
              6 CALL_FUNCTION            0
              9 POP_TOP             
             10 POP_BLOCK           
             11 JUMP_FORWARD            14 (to 28)

  3     >>   14 POP_TOP             
             15 POP_TOP             
             16 POP_TOP             

  4          17 LOAD_NAME                1 (failure)
             20 CALL_FUNCTION            0
             23 POP_TOP             
             24 JUMP_FORWARD             1 (to 28)
             27 END_FINALLY         
        >>   28 LOAD_CONST               0 (None)
             31 RETURN_VALUE     

Assuming that helloworld() raises an exception, the code follows from address 14 onwards. Because this except handler is generic, it makes sense that three POP_TOPs follow, and the failure() function call. However, afterwards, there is a 24 JUMP_FORWARD which "jumps over" 27 END_FINALLY, so that it doesn't get executed. What's its purpose here?

I noticed similar behaviour in versions 3.5, 3.6, 3.7 and 3.8. In 3.9 it seems like it's renamed to RERAISE: https://godbolt.org/z/YbeqPf3nx

Some context: After simplifying an obfuscated pyc and a lot of debugging I've found that such structure breaks uncompyle6


Solution

  • It resumes propagation of an exception when a finally block ends, or when there is no finally block and none of the except blocks match. It also handles resuming a return or continue that was suspended by a finally block.

    But you don't have a finally, and you've got a blanket except, which always matches, so the END_FINALLY never runs. It could be eliminated, but there's no handling in the bytecode compiler to do so.