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:
| Signal | Number | Default Action | Typical Trigger |
|---|---|---|---|
| SIGINT | 2 | Terminate | Ctrl+C in terminal |
| SIGTERM | 15 | Terminate | kill <pid>, Docker stop |
| SIGHUP | 1 | Terminate | Terminal closed, config reload |
| SIGALRM | 14 | Terminate | signal.alarm() timer |
| SIGCHLD | 17 | Ignore | Child process exited |
| SIGKILL | 9 | Terminate | Cannot be caught or ignored |
| SIGSTOP | 19 | Stop | Cannot 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.
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.