skip to content

What does signal.siginterrupt(sig, False) change, and why does CPython leave calls interruptible by default?

level: seniorimportance: nice to knowfreq 12%

answer

  1. A choice made when installing the handler
  2. False means the kernel restarts
  3. Python code runs only between bytecodes
  4. Restarting delays the handler indefinitely
  5. Registering a handler resets it

basics

~20 s

It asks the kernel to restart a system call that signal interrupts, instead of failing it. Python's own handler then cannot run until the blocked call returns, so an abort may be delayed for as long as the call blocks.

solid answer

~50 s

A POSIX handler can be installed with or without the restart behaviour. With it, the kernel silently reissues an interrupted call; without it, the call fails with `EINTR`. `signal.siginterrupt(sig, False)` turns restarting on for that signal. CPython deliberately does the opposite: `signal.signal` installs handlers in interruptible mode, because Python-level handlers only run between bytecodes — the process must actually leave the blocking call before your handler can execute. Letting the kernel restart the call means a handler registered to stop the program may not run until the read finishes, which for an idle socket is never. Since Python 3.5 the interpreter reissues the interrupted call itself, so you get prompt handler dispatch *and* the call completing, which is strictly better. One footgun: re-registering with `signal.signal` implicitly resets the flag back to interruptible, so a `signal.siginterrupt` call must come after the registration it modifies.

code

python · 8 lines
python
import signal

signal.signal(signal.SIGUSR1, lambda signum, frame: None)
signal.siginterrupt(signal.SIGUSR1, False)
print("kernel will now restart calls interrupted by SIGUSR1")

signal.signal(signal.SIGUSR1, lambda signum, frame: None)
print("re-registering silently reset it back to interruptible")

go deeper

for a junior

You are unlikely to need this call, but know that a signal handler carries a choice deciding whether an interrupted system call fails or is restarted by the kernel, and that Python's default is to let it fail so the interpreter regains control.

for a middle

Be able to state the polarity — False means restart — and explain why Python prefers interruptible: handlers are Python code, and Python code runs only between bytecodes, which requires leaving the blocking call first.

for a senior

Show why enabling restart is usually a regression: it delays handler execution for as long as the call blocks, and the setting silently disappears when anything re-registers the handler. Tie it to shutdown latency in a long-blocking service.

for a principal

Treat it as a question about where control is regained, and set the codebase convention: never depend on kernel-level restarting for correctness, because library code you do not own can re-register the handler and take the setting away.

## The kernel-level knob When a handler is installed for a signal, POSIX lets the installer choose what happens to a system call the signal interrupts. With the restart behaviour enabled, the kernel reissues the call itself once the handler returns, and the program never sees an error. With it disabled, the call fails with `EINTR` and the program must decide what to do. `signal.siginterrupt(signalnum, flag)` exposes that choice: `flag=True` means "interrupt calls" (fail with `EINTR`), `flag=False` means "restart them". The naming trips people up — the argument describes *interrupting*, so passing `False` is what turns kernel restarting on. ## Why CPython chooses interruptible At first glance kernel-level restarting looks like exactly what a high-level language wants: the call transparently completes and nobody writes retry code. CPython nonetheless registers handlers in interruptible mode, and `signal.signal` implicitly resets a signal to that mode whenever you register a handler for it. The reason is where Python-level handlers can run. A C signal handler may not execute Python code — it runs asynchronously on an arbitrary stack, and allocating or taking interpreter locks there is unsafe. So CPython's C handler only records that the signal arrived, and the callable you registered runs later, in the main thread, at the interpreter's next check between bytecodes. If the kernel restarts the interrupted call, the process goes straight back to blocking, and there is no next bytecode until the call finally returns. A handler installed to stop the program then does not run until the wait ends — which for a socket that never receives anything is never. Interruptible mode guarantees the call returns control to the interpreter promptly, so your handler runs when the signal arrives rather than whenever the I/O happens to finish. ## How it combines with automatic retry Interruptible mode used to have an obvious cost: the interrupted call surfaced as `InterruptedError`, and correct code had to retry it by hand. Since Python 3.5 (PEP 475) the interpreter reissues the call for you after running the handler, recomputing any remaining timeout from a monotonic deadline. That combination is why setting the restart behaviour is almost never useful in Python: you already get prompt handler dispatch *and* an uninterrupted-looking call, with a bounded timeout on top. Kernel-level restarting gives you only the second of the three. There is a case where people still reach for it: a signal used purely as a bookkeeping notification, with no Python-level handler at all, where you would rather the kernel absorb it entirely. Even then the gain is marginal, and the cost is that any future handler on that signal becomes latent — it will fire only when the blocking call ends. ## The reset footgun The documented behaviour of `signal.signal` is to reset the restart behaviour to interruptible for the signal it registers. So this is a no-op: ```python signal.siginterrupt(signal.SIGUSR1, False) signal.signal(signal.SIGUSR1, my_handler) # silently resets it ``` and the `signal.siginterrupt` call must follow the registration, not precede it. Since registration often happens in library setup code you do not control — a framework installing its own handler at start-up, or a reload path re-registering handlers — a setting made once at import time can quietly evaporate. That fragility is another reason to treat the flag as something you inspect while debugging rather than something you rely on. ## What to say about it Name the three layers cleanly: the kernel decides whether an interrupted call fails or restarts; the interpreter decides whether to reissue a failed call; and your handler decides, by returning or raising, whether that reissue happens at all. Python puts the decision in the middle layer on purpose, because that is the only layer that can run your code.

  • If the kernel restarts the call, does the C-level handler still run?
    Yes. Delivery is unaffected: the C handler runs immediately and records the signal. What changes is what happens to the interrupted call afterwards — the kernel reissues it rather than failing it — so the interpreter never regains control at a bytecode boundary and your Python-level handler stays pending until the call finally returns on its own.
  • Why is the argument's polarity so easy to get backwards?
    Because the parameter names the behaviour being enabled, not the one you usually want. `flag=True` means calls *are* interrupted, which is CPython's default; `flag=False` means they are restarted by the kernel. Reading it as "should this signal interrupt calls?" rather than "should calls restart?" keeps it straight.
  • When might enabling kernel restarting still be defensible?
    When a signal is pure bookkeeping — something arriving frequently that no Python-level handler needs to act on — and you would rather the kernel absorb the interruption than have the interpreter cycle through dispatch and reissue. The gain is small, and the cost is that a handler added later on that signal fires only when the blocking call ends, so document it or do not do it.

saying these in an interview costs you the question

  • Thinks False means calls are interrupted
  • Claims kernel restarting removes the need for handlers to run
  • Assumes the setting survives a later signal.signal call
  • Says Python code executes inside the C signal handler
  • Recommends kernel restarting as a general fix for EINTR

context