pythontry-catch

Why aren't exceptions caught with one-liner while statement?


I have a code with a one-liner while and a try-except statement which behaves weirdly.

This prints 'a' on Ctrl+C:

try:
    while True:
        pass
except KeyboardInterrupt:
    print("a")

and this too:

try:
    i = 0
    while True: pass
except KeyboardInterrupt:
    print("a")

but this doesn't, and it throws a traceback:

try:
    while True: pass
except KeyboardInterrupt:
    print("a")

and neither does this code:

try:
    while True: pass
    i = 0
except KeyboardInterrupt:
    print("a")

Addition some additional details.

In 3.11, the instruction JUMP_BACKWARD was added and seems invloved with this issue see: Disassembler for Python bytecode

In 3.12 when the code in the first and the 3rd blocks are disassembled the results are:

Cannot be caught:

  0           0 RESUME                   0

  2           2 NOP

  3     >>    4 JUMP_BACKWARD            1 (to 4)
        >>    6 PUSH_EXC_INFO

  4           8 LOAD_NAME                0 (KeyboardInterrupt)
             10 CHECK_EXC_MATCH
             12 POP_JUMP_IF_FALSE       11 (to 36)
             14 POP_TOP

  5          16 PUSH_NULL
             18 LOAD_NAME                1 (print)
             20 LOAD_CONST               1 ('a')
             22 CALL                     1
             30 POP_TOP
             32 POP_EXCEPT
             34 RETURN_CONST             2 (None)

  4     >>   36 RERAISE                  0
        >>   38 COPY                     3
             40 POP_EXCEPT
             42 RERAISE                  1
ExceptionTable:
  4 to 4 -> 6 [0]
  6 to 30 -> 38 [1] lasti
  36 to 36 -> 38 [1] lasti
None

Can be caught:

  0           0 RESUME                   0

  2           2 NOP

  3           4 NOP

  4     >>    6 NOP

  3           8 JUMP_BACKWARD            2 (to 6)
        >>   10 PUSH_EXC_INFO

  5          12 LOAD_NAME                0 (KeyboardInterrupt)
             14 CHECK_EXC_MATCH
             16 POP_JUMP_IF_FALSE       11 (to 40)
             18 POP_TOP

  6          20 PUSH_NULL
             22 LOAD_NAME                1 (print)
             24 LOAD_CONST               1 ('a')
             26 CALL                     1
             34 POP_TOP
             36 POP_EXCEPT
             38 RETURN_CONST             2 (None)

  5     >>   40 RERAISE                  0
        >>   42 COPY                     3
             44 POP_EXCEPT
             46 RERAISE                  1
ExceptionTable:
  4 to 8 -> 10 [0]
  10 to 34 -> 42 [1] lasti
  40 to 40 -> 42 [1] lasti
None

The main differences that jump out are the two additional NOP and the different targets for JUMP_BACKWARD.

Note: the exception really cannot be caught as this will also throw the exception in 3.12

try:
    try:
        while True: pass
    except KeyboardInterrupt:
        print("a")
except Exception:
    print("b")

Solution

  • Its a known CPython bug introduced in 3.11 and exists in 3.12.

    One of comments of the bug, mentioned that partial backport of this pull request looks to be the right direction to fix the bug.

    I built and tested following CPython versions from source using pyenv on Arch Linux with GCC 14.1.1 compiler:

    In 3.13.0b1, 3.13-dev and 3.14-dev the bug is fixed 😀👍 and exception handling works as expected.

    But 3.11-dev and 3.12-dev still have the bug.

    I hope it will be backport to existing stable 3.11 and 3.12 versions (in-time for inclusion in the next 3.11.10 and 3.12.4 bug-fix releases respectively).

    EDIT 1: 3.12.4 released in 2024-06-06: the bug didn't fixed.

    EDIT 2: 3.12.5 released in 2024-08-06: the bug didn't fixed.