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
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.