signal
import signal The signal module provides a way to handle asynchronous events in Python. Signals are notifications sent to a process by the operating system to indicate that something has happened—like a user pressing Ctrl+C, a timer expiring, or a child process terminating. This module lets you define custom handlers that run when specific signals arrive.
The module works primarily on Unix systems. Windows has limited signal support (only SIGINT, SIGTERM, SIGBREAK, SIGABRT, and SIGFPE are available). On Windows, signal.signal() can only be called with these signals, and functions like alarm() and pause() are not available.
Python installs default handlers for a few signals automatically. SIGPIPE is ignored (so write errors on pipes raise exceptions instead of killing your program), and SIGINT is translated to KeyboardInterrupt if the parent process hasn’t changed it.
Important Constraints
Python signal handlers have some quirks worth knowing about:
-
Handlers run in the main Python thread, even if the signal arrived in another thread. You cannot use signals for inter-thread communication.
-
Signal handlers don’t execute immediately when a signal arrives—they execute at the next Python bytecode instruction. This means a long-running C extension (like a regex on a large text) will run uninterrupted until it returns to Python.
-
If a signal handler raises an exception, it appears “out of thin air” in the main thread. This can happen between any two bytecode instructions, which is why complex code (especially code using locks) can behave unexpectedly when signals are involved.
-
Only the main thread can set signal handlers. Calling
signal.signal()from any other thread raises ValueError.
signal.signal()
Register a handler function to be called when a signal is received.
signal.signal(signalnum, handler)
Parameters:
| Parameter | Type | Description |
|---|---|---|
signalnum | int | Signal number (e.g., signal.SIGINT) |
handler | callable | Handler function or SIG_DFL/SIG_IGN |
The handler must be a callable accepting two arguments: the signal number and the current stack frame. You can also pass signal.SIG_IGN to ignore the signal or signal.SIG_DFL to use the default handler.
import signal
import sys
def handle_interrupt(signum, frame):
print("\nReceived interrupt signal!")
print(f"Signal number: {signum}")
sys.exit(0)
# Register handler for SIGINT (Ctrl+C)
signal.signal(signal.SIGINT, handle_interrupt)
# Now press Ctrl+C to see the handler run
print("Press Ctrl+C to trigger handler...")
import time
time.sleep(60)
Returns: The previous signal handler (callable, SIG_DFL, SIG_IGN, or None).
signal.raise_signal()
Send a signal to the calling process itself. This is useful for testing signal handlers or for self-signaling.
signal.raise_signal(signum)
Parameters:
| Parameter | Type | Description |
|---|---|---|
signum | int | Signal number to send |
import signal
def my_handler(signum, frame):
print(f"Caught signal {signum}")
signal.signal(signal.SIGUSR1, my_handler)
# Send SIGUSR1 to ourselves
signal.raise_signal(signal.SIGUSR1)
# Output: Caught signal 10
Added in Python 3.8.
signal.alarm()
Schedule a SIGALRM signal to be delivered after a specified number of seconds. Only one alarm can be scheduled at a time.
signal.alarm(time)
Parameters:
| Parameter | Type | Description |
|---|---|---|
time | int | Seconds until alarm (0 cancels any pending alarm) |
import signal
import time
def alarm_handler(signum, frame):
print("Alarm fired! 5 seconds have passed.")
signal.signal(signal.SIGALRM, alarm_handler)
print("Scheduling alarm in 5 seconds...")
signal.alarm(5)
# Do other work while waiting
time.sleep(3)
print("3 seconds later...")
# Wait for the alarm
time.sleep(3)
# Output:
# Scheduling alarm in 5 seconds...
# 3 seconds later...
# Alarm fired! 5 seconds have passed.
Returns: Number of seconds remaining on any previously scheduled alarm, or 0 if no alarm was pending.
Not available on Windows.
signal.pause()
Suspend the process until a signal is received. The registered handler (if any) will be called when the signal arrives.
signal.pause()
import signal
import time
def handler(signum, frame):
print("Signal received!")
signal.signal(signal.SIGUSR1, handler)
print("Waiting for SIGUSR1... (send with: kill -USR1 <pid>)")
signal.pause()
print("Continuing execution...")
Not available on Windows.
signal.getsignal()
Query the current handler registered for a signal.
signal.getsignal(signalnum)
Parameters:
| Parameter | Type | Description |
|---|---|---|
signalnum | int | Signal number to query |
Returns: The current handler—a callable, SIG_IGN, SIG_DFL, or None (if the handler wasn’t installed from Python).
import signal
# Check default handler for SIGINT
handler = signal.getsignal(signal.SIGINT)
print(f"SIGINT handler: {handler}")
# Output: <built-in function default_int_handler>
# Install custom handler
def custom_handler(signum, frame):
pass
signal.signal(signal.SIGINT, custom_handler)
print(f"After setting: {signal.getsignal(signal.SIGINT)}")
# Output: <function custom_handler at 0x...>
Common Signals
These are the most frequently used signals:
| Signal | Number | Description |
|---|---|---|
SIGINT | 2 | Interrupt from keyboard (Ctrl+C). Default raises KeyboardInterrupt. |
SIGTERM | 15 | Termination request. The polite way to ask a process to shut down. |
SIGALRM | 14 | Timer expired (from alarm() or setitimer()). |
SIGUSR1 | 10 | User-defined signal 1. Available for custom use. |
SIGUSR2 | 12 | User-defined signal 2. Available for custom use. |
SIGKILL | 9 | Kill signal. Cannot be caught, blocked, or ignored. Forces immediate termination. |
SIGSTOP | 19 | Stop signal. Cannot be caught or ignored. Pauses the process. |
SIGHUP | 1 | Hangup detected on terminal or death of controlling process. Common daemon notification. |
SIGCHLD | 17 | Child process stopped or terminated. Default is to ignore. |
import signal
# List all available signals on this platform
valid = signal.valid_signals()
print(f"Valid signals: {valid}")
# Get human-readable signal name
print(f"SIGINT name: {signal.Signals(signal.SIGINT).name}")
print(f"SIGTERM name: {signal.Signals(signal.SIGTERM).name}")
# Get description
print(f"SIGINT: {signal.strsignal(signal.SIGINT)}")
# Output: Interrupt from keyboard (Ctrl+C)
Examples
Implementing a timeout with alarm
import signal
import time
def timeout_handler(signum, frame):
raise TimeoutError("Operation timed out")
def slow_operation():
time.sleep(10)
return "completed"
# Set 2-second timeout
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(2)
try:
result = slow_operation()
print(f"Result: {result}")
except TimeoutError as e:
print(f"Error: {e}")
finally:
signal.alarm(0) # Cancel alarm if operation succeeded
Output:
Error: Operation timed out
Graceful shutdown on SIGTERM
import signal
import sys
import time
shutdown_requested = False
def handle_sigterm(signum, frame):
global shutdown_requested
print("Received SIGTERM, shutting down gracefully...")
shutdown_requested = True
signal.signal(signal.SIGTERM, handle_sigterm)
print("Server running... (send SIGTERM to test)")
while not shutdown_requested:
# Do work here
time.sleep(0.1)
print("Cleaned up resources.")
sys.exit(0)
Using user-defined signals for custom events
import signal
import os
def handle_usr1(signum, frame):
print("Reloading configuration!")
def handle_usr2(signum, frame):
print("Dumping debug info!")
signal.signal(signal.SIGUSR1, handle_usr1)
signal.signal(signal.SIGUSR2, handle_usr2)
print(f"Running with PID: {os.getpid()}")
print("Send SIGUSR1 to reload, SIGUSR2 to debug")
# In another terminal:
# kill -USR1 <pid>
# kill -USR2 <pid>
Common Patterns
Checking if code is running interactively
import signal
import sys
# Ignore SIGINT in non-interactive contexts
if not sys.stdin.isatty():
signal.signal(signal.SIGINT, signal.SIG_IGN)
Portable Ctrl+C handling
import signal
import sys
def graceful_shutdown(signum, frame):
print("\nShutting down...")
# Clean up resources here
sys.exit(0)
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)
Block and unblock signals (Unix)
import signal
# Block SIGUSR1 temporarily
old_mask = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGUSR1})
# SIGUSR1 is now blocked
# Do critical work...
# Restore previous mask
signal.pthread_sigmask(signal.SIG_SETMASK, old_mask)