You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
First of all, I know exactly how this bug is going to sound: batshit insane. I implore you to keep reading.
I've had a timer application for ages that uses signal.alarm() concurrently with input() so that it has a rudimentary way of displaying things in the background while still allowing user interaction on the CLI. The gist of it is this:
@staticmethod
def _input_timeout(prompt, timeout_secs):
class TimeExpiredException(Exception): pass
def raise_exception(signum, frame):
raise TimeExpiredException()
signal.signal(signal.SIGALRM, raise_exception)
try:
signal.alarm(timeout_secs)
result = input(prompt)
signal.alarm(0)
except TimeExpiredException:
result = None
return result
This program has mysteriously stopped working with Python 3.13.3 with an exceptionally strange error mode: my signal handler is called from somewhere I apparently do not expect (HelpFormatter), causing the whole process to terminate. This always happens after exactly 29 seconds when I run my process as user and after exactly 25 seconds when I run as root. It happens only when there is no __pycache__ present, i.e., when the Python bytecode is generated on startup.
I tried creating a minimal reproducer, which was super difficult. Even when I remove dead code from the Python script, the bug stops triggering. Some code removal was fine, but for example if I remove the wrapper around argparse, the bug disappears.
As I said. This sounds batshit crazy.
When the mystery signal is caught, it gives one final indication where it comes from:
reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m25,035s
user 0m0,025s
sys 0m0,011s
Note the message: Exception ignored in tp_clear of HelpFormatter: (this was running as root, you see that it took 25 seconds.)
Just so you believe me that it always triggers after the same time, here are three consecutive runs as root (I swear this is not copy paste, timings are accurate to millisecond level!):
reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m25,035s
user 0m0,025s
sys 0m0,011s
reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m25,035s
user 0m0,025s
sys 0m0,011s
reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m25,035s
user 0m0,025s
sys 0m0,010s
And here some runs as user:
reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m29,038s
user 0m0,029s
sys 0m0,010s
reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m29,039s
user 0m0,027s
sys 0m0,012s
reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException:
FINALLY
real 0m29,038s
user 0m0,029s
sys 0m0,009s
I'm running on 6.14.0-15-generic #15-Ubuntu. My CPU is an AMD Ryzen Threadripper PRO 3945WX 12-Cores. I'm using the Ubuntu-provided Python package (Ubuntu calls it 3.13.3-1):
Python 3.13.3 (main, Apr 8 2025, 19:55:40) [GCC 14.2.0] on linux
I cannot reproduce this issue on Debian with Python 3.11/ARM nor on another Ubuntu with Python 3.12/x86_64.
I cannot attach files (the reproducer) to this bug, so here it is as base64:
And pasting the above base64 blob in. Remember you have to clear __pycache__ to trigger the bug. I'm including the bytecode in case it contains something interesting.
Please someone tell me this is reproducible and I'm not completely losing my marbles.
CPython versions tested on:
3.13
Operating systems tested on:
Linux
The text was updated successfully, but these errors were encountered:
I haven't looked at the reproducer yet, but this is understandable if unexpected behavior. Exceptions raised during destructors (i.e., __del__ methods) get printed as unraisable exceptions and then swallowed:
That's not new behavior and it's pretty much a fundamental limitation of raising exception asynchronously, such as with signal.signal(). Most likely, the timing of things slightly changed in 3.13 such that you routinely get "unlucky" as to what's executing when the SIGALRM triggers.
Interesting, so are you saying that raising an exception is always undefined behavior in Python, always has been?
The thing that makes me quite curious is that I do believe that in the scenario, this does look like it is well-defined behavior. I'm setting signal.alarm() inside the try block, then cancel it before leaving the try block. Any SIGALRM should call the installed signal handler and the raised exception should be possible to stack-unwind (there is only a single main thread there). In this instance, it seems to be the case that the signal is delivered while outside the try...except block, which should never be possible.
Can you try to elaborate why this would be happening?
I would be fine with the exception being omitted (e.g., during __del__, as you described) and being silently discarded. But somehow my program is not always able to catch the raised exception, which is indeed a problem.
Thanks for taking the time to read through my report! Cheers.
Bug report
Bug description:
First of all, I know exactly how this bug is going to sound: batshit insane. I implore you to keep reading.
I've had a timer application for ages that uses
signal.alarm()
concurrently withinput()
so that it has a rudimentary way of displaying things in the background while still allowing user interaction on the CLI. The gist of it is this:This program has mysteriously stopped working with Python 3.13.3 with an exceptionally strange error mode: my signal handler is called from somewhere I apparently do not expect (
HelpFormatter
), causing the whole process to terminate. This always happens after exactly 29 seconds when I run my process as user and after exactly 25 seconds when I run as root. It happens only when there is no__pycache__
present, i.e., when the Python bytecode is generated on startup.I tried creating a minimal reproducer, which was super difficult. Even when I remove dead code from the Python script, the bug stops triggering. Some code removal was fine, but for example if I remove the wrapper around
argparse
, the bug disappears.As I said. This sounds batshit crazy.
When the mystery signal is caught, it gives one final indication where it comes from:
Note the message:
Exception ignored in tp_clear of HelpFormatter:
(this was running as root, you see that it took 25 seconds.)Just so you believe me that it always triggers after the same time, here are three consecutive runs as root (I swear this is not copy paste, timings are accurate to millisecond level!):
And here some runs as user:
I'm running on 6.14.0-15-generic #15-Ubuntu. My CPU is an AMD Ryzen Threadripper PRO 3945WX 12-Cores. I'm using the Ubuntu-provided Python package (Ubuntu calls it 3.13.3-1):
I cannot reproduce this issue on Debian with Python 3.11/ARM nor on another Ubuntu with Python 3.12/x86_64.
I cannot attach files (the reproducer) to this bug, so here it is as base64:
You can simply unpack it by doing:
And pasting the above base64 blob in. Remember you have to clear
__pycache__
to trigger the bug. I'm including the bytecode in case it contains something interesting.Please someone tell me this is reproducible and I'm not completely losing my marbles.
CPython versions tested on:
3.13
Operating systems tested on:
Linux
The text was updated successfully, but these errors were encountered: