Python Signal Handling — Core Concepts

What Signals Are

Signals are asynchronous notifications sent by the operating system to a process. They’re the primary mechanism for inter-process communication and lifecycle management on Unix systems (Linux, macOS). Windows supports a limited subset.

Common signals:

SignalNumberDefault ActionTypical Trigger
SIGINT2TerminateCtrl+C in terminal
SIGTERM15Terminatekill <pid>, Docker stop
SIGHUP1TerminateTerminal closed, config reload
SIGALRM14Terminatesignal.alarm() timer
SIGCHLD17IgnoreChild process exited
SIGKILL9TerminateCannot be caught or ignored
SIGSTOP19StopCannot be caught or ignored

Registering a Signal Handler

Use signal.signal() to register a function that runs when a signal is received:

import signal
import sys

def handle_sigterm(signum, frame):
    print("Received SIGTERM, shutting down gracefully...")
    cleanup()
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)

The handler receives two arguments: the signal number and the current stack frame (useful for debugging where the program was interrupted).

Graceful Shutdown Pattern

The most common use case — clean up resources when asked to stop:

import signal
import sys

running = True

def shutdown_handler(signum, frame):
    global running
    print(f"\nReceived signal {signum}, initiating shutdown...")
    running = False

signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)

# Main loop
while running:
    process_next_item()

# Cleanup after loop exits
close_connections()
flush_buffers()
print("Clean shutdown complete")

This pattern lets the program finish its current work item before exiting, rather than dying mid-operation.

Timeout with SIGALRM

On Unix, signal.alarm() schedules a SIGALRM after N seconds — useful for enforcing time limits:

import signal

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(10)  # 10-second timeout

try:
    result = slow_operation()
    signal.alarm(0)  # cancel the alarm
except TimeoutError:
    print("Operation took too long")

Warning: signal.alarm() only works on Unix, and only one alarm can be pending at a time. For more flexible timeouts, use threading.Timer or asyncio timeouts.

Signal Handling in Multi-Threaded Programs

A critical constraint: signal handlers can only be registered in the main thread. Signals are always delivered to the main thread. This means:

import signal
import threading

# This works:
signal.signal(signal.SIGINT, handler)  # in main thread

# This raises ValueError:
def worker():
    signal.signal(signal.SIGINT, handler)  # NOT in main thread

t = threading.Thread(target=worker)
t.start()  # → ValueError: signal only works in main thread

To communicate a signal to worker threads, set a shared flag (Event or boolean) in the handler.

Special Handler Values

# Ignore the signal entirely
signal.signal(signal.SIGHUP, signal.SIG_IGN)

# Restore default behavior
signal.signal(signal.SIGINT, signal.SIG_DFL)

SIG_IGN is useful for daemon processes that should survive terminal disconnection (SIGHUP). SIG_DFL resets to Python’s default — for SIGINT, that means KeyboardInterrupt.

Python’s Default SIGINT Behavior

Python installs a default SIGINT handler that raises KeyboardInterrupt. This is why Ctrl+C produces a traceback instead of silently killing the process. If you override SIGINT, you lose this behavior:

signal.signal(signal.SIGINT, my_handler)
# Ctrl+C no longer raises KeyboardInterrupt
# It calls my_handler instead

To restore it: signal.signal(signal.SIGINT, signal.default_int_handler).

Common Misconception

“Signal handlers run immediately when the signal arrives.” In CPython, signal handlers are deferred — the interpreter checks for pending signals between bytecodes (roughly every few milliseconds). This means a handler won’t interrupt a C extension call or a system call in progress. The signal is queued and delivered at the next safe point.

One thing to remember: Register SIGTERM and SIGINT handlers for any long-running Python process — set a shutdown flag, let the current work item finish, clean up resources, then exit. It’s the difference between a graceful shutdown and data corruption.

pythonsystemsadvanced

See Also

  • Python Select And Polling How Python watches many connections at once without wasting energy — like a lifeguard scanning an entire pool.
  • Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
  • Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.
  • Python 310 New Features Python 3.10 gave programmers a shape-sorting machine, friendlier error messages, and cleaner ways to say 'this or that' in type hints.
  • Python 311 New Features Python 3.11 made everything faster, error messages smarter, and let you catch several mistakes at once instead of stopping at the first one.