skip to content

Signals and Interrupts

Asynchronous notifications from the kernel and what the interpreter does with them: which thread may register a handler, when it actually runs, timers you arm yourself, and a clean shutdown.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

20

How do you bound a blocking call with signal.alarm when it takes no timeout?

level: juniorimportance: must knowfreq 42%

answer

  1. The kernel can interrupt you on a clock
  2. One signal whose default is fatal
  3. The handler raises; the call site unwinds
  4. Disarming belongs in a finally block
  5. Whole seconds, Unix only, main thread

basics

~10 s

Install a SIGALRM handler with signal.signal that raises an exception, call signal.alarm(seconds) just before the blocking call, and call signal.alarm(0) in a finally block. The handler's exception unwinds the blocked call.

solid answer

~40 s

`signal.alarm(n)` asks the kernel to send `SIGALRM` to the process after roughly `n` whole seconds. On its own that kills the process, because `SIGALRM`'s default disposition is terminate, so you first install a Python handler with `signal.signal(signal.SIGALRM, handler)` whose body raises — usually `TimeoutError`. CPython runs that handler in the main thread between bytecodes; when the guarded call is a blocking syscall the signal interrupts it and the exception propagates out of the call site. Always disarm in a `finally` with `signal.alarm(0)`, or the timer survives a fast success and fires later inside unrelated code. `signal.alarm` returns the seconds left on any previously armed alarm, so arming a second one silently replaces the first. It is Unix-only and whole-seconds only; `signal.setitimer` gives fractional seconds.

code

python · 13 lines
python
import signal

def on_alarm(signum, frame):
    raise TimeoutError("took too long")

signal.signal(signal.SIGALRM, on_alarm)
signal.alarm(1)
try:
    signal.pause()          # blocks until a signal arrives
except TimeoutError as exc:
    print("bounded:", exc)
finally:
    signal.alarm(0)         # disarm, including on the success path

go deeper

for a junior

Be ready to write the four lines from memory: install a handler that raises, arm with signal.alarm, guard the call, disarm with signal.alarm(0) in finally. Knowing that an unhandled SIGALRM kills the process is the detail that separates recall from having actually run it.

for a middle

Explain the mechanics: the handler runs between bytecodes in the main thread, the exception is what makes the deadline visible, and there is only one such timer per process. Be able to say why the finally block is load-bearing rather than tidy.

for a senior

Show the production judgement — restore the previous handler, never leave a stale alarm armed, and know that a blocking syscall is interruptible while a long compiled call is not. Say when you would use a call's own timeout argument instead.

for a principal

Own the policy question: an out-of-band signal is a last resort for calls that expose no deadline, and a codebase that sprinkles signal.alarm around is really telling you its I/O layer lacks timeouts. Decide where deadlines live and push them into the transport.

Plenty of calls in a Unix Python process have no timeout of their own: a compiled library entry point, a device read, a legacy client that exposes no deadline argument. The oldest and still the most common answer is to make the kernel interrupt you on a clock, and to turn that interruption into an ordinary Python exception. ### The three moving parts **1. A disposition that is not the default.** `SIGALRM` is signal number 14 and its default disposition is *terminate the process*. If you arm an alarm and never register a handler, the process dies when it fires — no traceback, no `finally` blocks, no `atexit` callbacks, and a shell exit status of 142 (128 + 14). So the first line of the pattern is always `signal.signal(signal.SIGALRM, on_alarm)`. **2. A handler that raises.** The handler receives `(signum, frame)` and does one useful thing: `raise TimeoutError(...)`. It is the *raise* that produces the timeout — `signal.alarm` itself never raises anything. Because CPython only runs Python-level signal handlers between bytecode instructions in the main thread, the exception surfaces at whatever bytecode boundary the main thread reaches next. For a blocking syscall that boundary comes almost immediately: the signal interrupts the syscall, the interpreter notices the pending handler, runs it, and the exception propagates out of the call site as if the call had raised. **3. Arming and disarming.** `signal.alarm(n)` schedules the signal for approximately `n` seconds from now; `signal.alarm(0)` cancels a pending one. The argument is an integer — `signal.alarm(0.5)` raises `TypeError`, because whole seconds are all the underlying call accepts. The return value is the number of seconds remaining on the previously scheduled alarm, or `0` if none was armed, which is the only bookkeeping the API gives you. ### Why the finally block is not optional There is exactly **one** of these timers per process. Arming a five-second deadline around a call that finishes in forty milliseconds leaves 4.96 seconds on the clock. Nobody cancelled it, so it fires later — inside a different function, inside cleanup code, inside a long test run — and raises `TimeoutError` from a stack frame that never asked for a deadline. That is one of the classic ways this pattern turns into a mystery bug, and the cure is mechanical: arm in `try`, disarm in `finally`. The same one-timer-per-process fact explains why nesting does not work naively. An inner deadline overwrites the outer one; when the inner region exits and calls `signal.alarm(0)`, it has also cancelled the outer deadline. A serious helper saves the returned remaining time and restores it, or refuses to nest. ### What it can and cannot interrupt The mechanism is only as good as the interpreter's chance to run the handler. A blocking *syscall* is interrupted by the signal, so the eval loop gets control back quickly and the exception lands where you want it. A long **compiled** computation that neither returns to the interpreter nor checks for pending signals is a different story: the handler cannot run until that call finally returns, so the deadline is simply late — sometimes uselessly so. The C-level handler CPython installs does almost nothing but set a flag; all the real work waits for the eval loop. ### The shape to write ```python import signal def on_alarm(signum, frame): raise TimeoutError("sensor read exceeded its budget") previous = signal.signal(signal.SIGALRM, on_alarm) signal.alarm(3) try: result = read_from_device() # no timeout argument of its own finally: signal.alarm(0) signal.signal(signal.SIGALRM, previous) ``` Capturing and restoring the previous handler matters once more than one piece of code in the process cares about `SIGALRM`; `signal.signal` returns the old handler for exactly that reason, and it may be `signal.SIG_DFL`. ### Limits worth saying out loud This is a Unix-only mechanism — there is no `SIGALRM` on Windows. It is whole-seconds only; `signal.setitimer(signal.ITIMER_REAL, 0.25)` is the fractional-resolution sibling and shares the same underlying timer. And it only works from the process's main thread, because that is where handler registration is allowed and where the handler runs. If the call you want to bound already accepts a deadline of its own, use that instead: a real timeout argument beats an out-of-band signal every time. `signal.alarm` is the tool for the calls that offer you nothing.

  • What happens if you arm signal.alarm(3) but never install a SIGALRM handler?
    The default disposition for SIGALRM is to terminate the process, so it simply dies when the timer fires. There is no traceback, `finally` blocks and `atexit` callbacks do not run, and the shell reports exit status 142, which is 128 plus signal number 14. Installing a handler is the first line of the pattern, not an optional refinement.
  • Why does signal.alarm return a number, and what happens if two deadlines nest?
    It returns the seconds still pending on the previously armed alarm, or 0 if none. There is one such timer per process, so an inner deadline overwrites the outer one, and the inner region's `signal.alarm(0)` then cancels both. A helper that must nest has to save that returned remainder and re-arm it on the way out.
  • Can this pattern interrupt a long compiled computation that never returns to the interpreter?
    No. CPython's C-level handler only sets a flag; the Python handler runs between bytecodes in the eval loop. A compiled call that neither returns nor checks for pending signals keeps going, and the exception appears only once it finishes. The pattern works well for blocking syscalls, and poorly for CPU-bound compiled work — there a child process you can kill is the honest answer.

It is a kitchen timer wired to the fire alarm: useful only because you first decided what the bell should do, and dangerous if you walk out of the kitchen without winding it back to zero.

saying these in an interview costs you the question

  • Thinks signal.alarm itself raises the timeout exception
  • Forgets signal.alarm(0), leaving a stale timer armed
  • Believes an unhandled SIGALRM is quietly ignored
  • Passes a float to signal.alarm and expects sub-second timing
  • Assumes the pattern also works on Windows
  • Never restores the previously installed SIGALRM handler

context

open as a page

How do signal.SIGINT and signal.SIGTERM differ in a Python process by default?

level: juniorimportance: must knowfreq 60%

basics

~20 s

CPython installs its own default handler for signal.SIGINT that raises KeyboardInterrupt in the main thread, so finally blocks and atexit hooks still run. signal.SIGTERM keeps the operating system default, which terminates the process immediately with no Python cleanup.

open as a page

What did PEP 475 change in Python 3.5 about system calls interrupted by a signal?

level: middleimportance: must knowfreq 30%

basics

~20 s

Since Python 3.5, the interpreter reissues a system call that a signal interrupted instead of raising InterruptedError, after running the Python-level handler and recomputing any remaining timeout. Hand-written EINTR retry loops around stdlib calls became dead code.

open as a page

Why can a handler installed with signal.signal() run well after the signal actually arrives?

level: middleimportance: must knowfreq 58%

basics

~10 s

CPython installs its own C handler, which only records that the signal is pending. Your Python function runs later, when the main thread of the main interpreter reaches its next bytecode boundary check.

open as a page

Why does a Python process running as PID 1 ignore signal.SIGTERM?

level: middleimportance: must knowfreq 45%

basics

~10 s

The kernel gives PID 1 no default action: a signal left at its default disposition is discarded, not acted on. CPython installs a handler for SIGINT but not SIGTERM, so call signal.signal(signal.SIGTERM, handler) yourself.

open as a page

Why does atexit.register cleanup not run when a Python process is killed by SIGTERM?

level: middleimportance: must knowfreq 50%

basics

~20 s

Functions registered with atexit.register run only during a normal interpreter shutdown. The default disposition for signal.SIGTERM destroys the process in the kernel, so no further bytecode executes and no cleanup path is reached. Install a terminate handler to get one.

open as a page

Why does Ctrl-C during a blocking socket.socket.recv raise KeyboardInterrupt instead of resuming the read?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Ctrl-C delivers SIGINT, and Python's default SIGINT handler raises KeyboardInterrupt. Since Python 3.5 the interpreter reissues an interrupted call only when the handler returns normally; a handler that raises aborts the retry, so the exception leaves recv.

open as a page

What is the difference between signal.SIG_DFL and signal.SIG_IGN in signal.signal()?

level: juniorimportance: should knowfreq 46%

basics

~10 s

signal.SIG_DFL restores the operating system's default action for that signal, such as terminating the process. signal.SIG_IGN discards the signal entirely, so neither a handler nor the default action runs.

open as a page

How can a Python program tell at startup that it is running as PID 1?

level: juniorimportance: should knowfreq 20%

basics

~20 s

Compare os.getpid() with 1; for the first process of a PID namespace os.getppid() also returns 0. Both report namespace-local numbers, so a process can be PID 1 inside an isolated environment while carrying a completely different PID outside it.

open as a page

Why can't signal.alarm time out a blocking call running in a worker thread?

level: middleimportance: should knowfreq 33%

basics

~20 s

CPython runs every Python signal handler in the main thread. The SIGALRM handler's exception unwinds the main thread's stack, while the worker thread stays blocked exactly where it was. Signals cannot interrupt one specific thread.

open as a page

How should a Python PID 1 forward signal.SIGTERM to the children it started?

level: middleimportance: should knowfreq 30%

basics

~20 s

A signal sent to PID 1 reaches only that process; children see nothing. Record the request in the handler, then have the main loop forward it with subprocess.Popen.send_signal, wait on a shared deadline, and escalate to signal.SIGKILL.

open as a page

A telemetry collector arms signal.alarm per sensor read; why do descriptors leak and unrelated code raise TimeoutError?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Almost certainly the alarm is never disarmed on the fast path, so it fires later in code that never asked for a deadline. And because the exception lands at an arbitrary bytecode boundary, it can arrive after a socket is opened but before anything is responsible for closing it.

open as a page

A retry loop re-passes its 30-second timeout and catches Exception on every interruption; why does the wait never expire?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Re-passing the original timeout restarts the clock on every interruption, so a wait interrupted more often than every 30 seconds never expires. Catching Exception also swallows the abort a signal handler raised, so the shutdown request disappears with it.

open as a page

Why is doing real work inside a signal.signal() handler unsafe, and what belongs there instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The handler runs on the main thread between two bytecode instructions, so it re-enters code that was mid-update and can tear a multi-step change or deadlock on a lock that code already holds. Set a flag and return.

open as a page

How does a Python program at PID 1 reap the orphans it inherits?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Orphans are reparented to PID 1, and a dead child stays a zombie until its parent collects it. A Python program there must loop on os.waitpid(-1, os.WNOHANG) until it reports nothing more, on signal.SIGCHLD or each main-loop pass.

open as a page

An email-digest sender gets SIGTERM with a 30-second window before SIGKILL — how do you drain without truncating a digest?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Catch signal.SIGTERM with a handler that only sets a flag, stop pulling new digests, finish the ones in flight against a deadline shorter than the 30-second window, and exit. signal.SIGKILL cannot be caught, so anything unfinished must be safely retryable.

open as a page

How would you set the SIGTERM drain budget for a fleet of Python workers, and decide what to abandon?

level: principalimportance: should knowfreq 30%

basics

~20 s

Derive the budget from measured work-item durations — a high percentile plus margin, not the mean or the maximum — keep it inside the supervisor's window, and make abandoning work cheap by keeping every unit idempotent.

open as a page

What does signal.setitimer with signal.ITIMER_REAL do that signal.alarm cannot?

level: middleimportance: nice to knowfreq 18%

basics

~10 s

signal.setitimer takes float seconds, so it can arm a sub-second deadline, and an optional interval argument that re-arms the timer for a repeating SIGALRM. signal.alarm is one-shot and whole-seconds. signal.getitimer reports what is left.

open as a page

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

level: seniorimportance: nice to knowfreq 12%

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.

open as a page

What does signal.set_wakeup_fd() solve that a plain signal.signal() handler cannot?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

It makes CPython's C-level handler write the signal number to a file descriptor the moment the signal lands, so a thread blocked in a file-descriptor wait such as select.select wakes immediately instead of waiting for the bytecode loop.

open as a page