The signal module handles Unix signals—the OS-level notifications for events like Ctrl+C, termination requests, and alarms.

Basic Signal Handler

import signal
import sys
 
def handler(signum, frame):
    print(f"Received signal {signum}")
    sys.exit(0)
 
# Handle Ctrl+C
signal.signal(signal.SIGINT, handler)
 
# Handle termination request
signal.signal(signal.SIGTERM, handler)
 
# Program runs...
while True:
    pass

Common Signals

import signal
 
# SIGINT (2): Interrupt from keyboard (Ctrl+C)
signal.signal(signal.SIGINT, handler)
 
# SIGTERM (15): Termination request
signal.signal(signal.SIGTERM, handler)
 
# SIGHUP (1): Terminal hangup
signal.signal(signal.SIGHUP, handler)
 
# SIGALRM (14): Timer alarm
signal.signal(signal.SIGALRM, handler)
 
# SIGCHLD (17): Child process stopped or terminated
signal.signal(signal.SIGCHLD, handler)

Note: SIGKILL and SIGSTOP cannot be caught or ignored.

Graceful Shutdown Pattern

import signal
import sys
import time
 
shutdown_requested = False
 
def shutdown_handler(signum, frame):
    global shutdown_requested
    print("\nShutdown requested, finishing current work...")
    shutdown_requested = True
 
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
 
def main():
    while not shutdown_requested:
        # Do work
        print("Working...")
        time.sleep(1)
    
    print("Cleanup complete, exiting")
    sys.exit(0)
 
main()

Timeout with SIGALRM

import signal
 
class TimeoutError(Exception):
    pass
 
def timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")
 
def with_timeout(seconds):
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Set alarm
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(seconds)
            try:
                return func(*args, **kwargs)
            finally:
                signal.alarm(0)  # Cancel alarm
        return wrapper
    return decorator
 
@with_timeout(5)
def slow_operation():
    import time
    time.sleep(10)  # Will timeout after 5 seconds
 
try:
    slow_operation()
except TimeoutError:
    print("Operation timed out!")

Context Manager for Timeout

import signal
from contextlib import contextmanager
 
@contextmanager
def timeout(seconds):
    def handler(signum, frame):
        raise TimeoutError(f"Timed out after {seconds} seconds")
    
    old_handler = signal.signal(signal.SIGALRM, handler)
    signal.alarm(seconds)
    try:
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, old_handler)
 
# Usage
with timeout(5):
    slow_network_call()

Ignore Signals

import signal
 
# Ignore SIGINT (Ctrl+C won't stop program)
signal.signal(signal.SIGINT, signal.SIG_IGN)
 
# Reset to default behavior
signal.signal(signal.SIGINT, signal.SIG_DFL)

Get Current Handler

import signal
 
# Get current handler
current = signal.getsignal(signal.SIGINT)
 
if current == signal.SIG_DFL:
    print("Default handler")
elif current == signal.SIG_IGN:
    print("Signal ignored")
else:
    print(f"Custom handler: {current}")

Signal in Threads

Signals can only be handled in the main thread:

import signal
import threading
 
def handler(signum, frame):
    print("Signal received in main thread")
 
# Must be called from main thread
signal.signal(signal.SIGINT, handler)
 
# In other threads, use events or queues
stop_event = threading.Event()
 
def worker():
    while not stop_event.is_set():
        # Work
        pass
 
def signal_handler(signum, frame):
    stop_event.set()
 
signal.signal(signal.SIGTERM, signal_handler)

Practical Examples

Web Server Graceful Shutdown

import signal
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler
 
server = None
 
def shutdown(signum, frame):
    print("\nShutting down server...")
    if server:
        server.shutdown()
    sys.exit(0)
 
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
 
server = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
print("Server running on port 8000")
server.serve_forever()

Worker Process Management

import signal
import os
 
workers = []
 
def reap_children(signum, frame):
    """Handle SIGCHLD to clean up zombie processes."""
    while True:
        try:
            pid, status = os.waitpid(-1, os.WNOHANG)
            if pid == 0:
                break
            workers.remove(pid)
            print(f"Worker {pid} exited")
        except ChildProcessError:
            break
 
signal.signal(signal.SIGCHLD, reap_children)

Reload Configuration

import signal
 
config = {}
 
def load_config():
    global config
    with open('config.json') as f:
        config = json.load(f)
    print("Configuration reloaded")
 
def reload_handler(signum, frame):
    load_config()
 
signal.signal(signal.SIGHUP, reload_handler)
load_config()
 
# Send SIGHUP to reload: kill -HUP <pid>

Signal Names

import signal
 
# Get signal name from number
signal.Signals(2).name  # 'SIGINT'
 
# List all signals
for sig in signal.Signals:
    print(f"{sig.name}: {sig.value}")

Quick Reference

import signal
 
# Set handler
signal.signal(signal.SIGTERM, handler)
 
# Handler signature
def handler(signum, frame):
    pass
 
# Special handlers
signal.SIG_IGN   # Ignore signal
signal.SIG_DFL   # Default behavior
 
# Get current handler
signal.getsignal(signal.SIGINT)
 
# Set alarm (SIGALRM after n seconds)
signal.alarm(n)
signal.alarm(0)  # Cancel
 
# Pause until signal received
signal.pause()
SignalDefaultCommon Use
SIGINTTerminateCtrl+C
SIGTERMTerminateGraceful stop
SIGKILLTerminateForce kill (uncatchable)
SIGHUPTerminateConfig reload
SIGALRMTerminateTimeout
SIGCHLDIgnoreChild process events

signal gives you control over how your program responds to OS events. Use it for graceful shutdowns, timeouts, and process management.

React to this post: