You will need to use `dis` to see what actually done by bytecode. $ ./python -m dis tests.py Disassembly of <code object error at 0x7fbb69da5340, file "tests.py", line 8>: 9 0 SETUP_EXCEPT 4 (to 6) 10 2 POP_BLOCK 4 JUMP_FORWARD 12 (to 18) 11 >> 6 POP_TOP 8 POP_TOP 10 POP_TOP 12 12 POP_EXCEPT 14 JUMP_FORWARD 2 (to 18) 16 END_FINALLY 14 >> 18 LOAD_CONST 0 (None) 20 RETURN_VALUE this is the actual bytecode in `error()`. When the eval loop hit the line 9, it will perform bytecode `SETUP_EXCEPT`, thus push a block into the blockstack. Your code in trace wrote that `and frame.f_lineno > 12`, thus it will run line 12 and perform POP_EXCEPT to pop out the blockstack. then goto line 13, catch by trace if statement, f_lineno changed to 12, and perform `POP_EXCEPT` again, there isn't any block inside the blockstack, thus it will get a "XXX block stack underflow".
This is fixed in 3.8. Traceback (most recent call last): File ".py", line 15, in error() File ".py", line 14, in error pass File ".py", line 14, in error pass File ".py", line 4, in trace frame.f_lineno = 12 ValueError: can't jump into or out of a 'finally' block