Python signal handlers can crash when print is called reentrantly

Python signal handlers are not bound by the usual C-level restrictions on what a signal handler may safely call, because CPython does not run the user's Python handler inside the low-level C signal handler where those restrictions apply. Instead, CPython defers the call until later, when the interpreter is back in a consistent state. But this deferred-execution model has a side effect the post explores: Python signal handlers are themselves reentrant. If a new signal arrives while a Python handler is already running, the handler can be called again in the middle of the first call, before it finishes.

To see what that reentrancy can actually do, the post sets up a minimal test: a handler for the SIGUSR1 signal whose only job is printing the words signal received, registered with signal.signal, then fired at the running process 50 times in rapid succession using a shell loop that calls kill on the process's own PID, run through subprocess.run.

Running that test crashes the program. Python's traceback shows the handler being reentered again and again: three explicit stack frames of the handler calling print are printed, followed by the note 'Previous line repeated 2 more times', which marks two further reentrant calls collapsed into that single line, before the program dies with RuntimeError: reentrant call inside <_io.BufferedWriter name=''>. What that error means is that an earlier call to print, still writing to stdout, gets called into again by a later signal's handler invocation on the very same buffered writer object; Python's io module detects that and refuses to allow it, raising an exception rather than silently corrupting the output stream.

The author frames this as an edge case rather than a warning: it is unlikely that a real program would face the extreme conditions needed to reproduce it, and even when it does happen, failing with a Python exception is, in the author's view, more palatable than what unsafe signal handling can do at the C level, where the possible failure modes include deadlock, corrupted data structures, and silent failures. The conclusion the post draws is that this is signals trivia rather than a practical concern for how to write signal handlers, though the author still recommends against doing non-trivial work inside one.

Key facts

  • CPython does not run a user's Python signal handler inside the low-level C signal handler where the usual safety restrictions apply; it defers the call to a later point when the interpreter is in a consistent state, which is why those restrictions do not bind ordinary Python signal-handling code.
  • Python signal handlers are reentrant: if a new signal arrives while a handler is already running, the handler can be invoked again in the middle of the first call, before it returns.
  • A stress test uses a shell loop to fire 50 SIGUSR1 signals at a process in rapid succession, targeting a handler whose only job is calling print.
  • The test crashes with RuntimeError: reentrant call inside <_io.BufferedWriter name=''>, after a traceback shows three explicit reentrant calls into the handler plus a note marking two more collapsed into it.
  • The author calls this signals trivia rather than a practical risk since real programs are unlikely to hit the extreme conditions required, but still advises against doing non-trivial work inside a signal handler, and considers a raised exception preferable to the deadlocks, corrupted data structures, or silent failures that unsafe C-level handlers risk.

Why it matters

Python already spares ordinary code from the C-level rules on what a signal handler may safely call, by deferring execution of the Python handler until the interpreter is in a consistent state. This post matters because it shows that protection is not absolute. Python handlers are themselves reentrant, and under the right conditions that reentrancy can surface as a real crash rather than stay a theoretical footnote. The point is not that Python's signal handling is broken; it is that letting Python handle the hard part for you has a specific, demonstrable edge.

Who it affects

This concerns anyone who installs a custom Python signal handler with signal.signal and does more than the bare minimum inside it, especially a handler that calls print or otherwise writes output. It matters most for code that might receive the same signal many times in a short window, and matters far less for a handler that only expects to run once in a while.

How to use it

The source's own guidance is to keep a Python signal handler doing as little as possible, since it still advises against non-trivial work inside one even though the reentrancy crash itself needs extreme conditions to trigger. It also means a call to print inside a handler cannot be assumed unconditionally safe under a rapid burst of repeated signals, since that is exactly the condition that produced a RuntimeError here. The source does not test or make claims about any function besides print, so this should not be stretched into guidance about what else is or is not safe to call from a handler.

How solid is it

The evidence is a first-hand, reproducible experiment: the post shows the exact handler code, the exact shell loop used to fire 50 signals, and the exact traceback and RuntimeError produced when running it. That is solid as far as it goes. What the source does not state is which operating system or Python version the test ran on, so the precise conditions for reproducing it elsewhere are not given, and it does not say how many times the test was run on the author's own machine.

Risks and caveats

The post itself frames the finding as trivia rather than a warning: triggering the crash took a purpose-built stress test of 50 signals fired in rapid succession, conditions the author says a real program is unlikely to face. The source does not say how common it is for real-world signal handlers to call print, and it does not identify any function besides print as safe or unsafe to call from a handler, so the finding should not be generalized beyond what was actually tested.

“it is unlikely that a real program would face these conditions, and even so, failing with an exception is more palatable than the possible consequences of unsafe signal handlers in C, which include deadlock, corrupted data structures, and silent failures”

— the post's author