Python - Python Signal Handling with the signal Module

Python provides the signal module to handle signals sent to a running program by the operating system. A signal is a notification that tells a process that a particular event has occurred. For example, a user pressing Ctrl+C in the terminal generates an interrupt signal that Python normally converts into a KeyboardInterrupt exception.

Signal handling is useful when a program needs to respond gracefully to events such as interruption, termination, alarms, or other operating-system notifications. Instead of allowing a program to stop suddenly, a signal handler can perform cleanup operations, save important information, close resources, or initiate a controlled shutdown.

1. What Is a Signal?

A signal is a software notification sent to a process. Operating systems use signals to communicate events to running programs.

Some commonly encountered signals include:

  • SIGINT: Interrupt signal, commonly generated by pressing Ctrl+C.

  • SIGTERM: Request to terminate a process.

  • SIGKILL: Forces a process to terminate. It cannot be caught or handled by a Python signal handler.

  • SIGALRM: Alarm signal generated after a specified amount of time.

  • SIGHUP: Traditionally indicates that a terminal or controlling process has been disconnected.

  • SIGUSR1 and SIGUSR2: User-defined signals available on systems that support them.

The exact signals available can vary between operating systems.

2. The signal Module

Python's built-in signal module provides functions for registering signal handlers and interacting with operating-system signals.

It can be imported using:

import signal

The signal.signal() function is used to associate a signal with a handler function.

Its general form is:

signal.signal(signal_number, handler)

Here, signal_number identifies the signal, while handler is the function that should execute when the signal is received.

3. Creating a Signal Handler

A signal handler is a function that Python executes when the specified signal is received.

Example:

import signal
import time

def handle_signal(signum, frame):
    print("Signal received. Preparing to stop the program.")

signal.signal(signal.SIGINT, handle_signal)

while True:
    print("Program is running...")
    time.sleep(2)

In this example, SIGINT is associated with handle_signal().

When the user presses Ctrl+C, Python invokes the handler instead of immediately terminating the program.

The handler receives two arguments:

signum
frame

signum contains the number of the received signal, while frame represents the current execution frame.

4. Handling SIGTERM

SIGTERM is commonly used to request that a process terminate. It is particularly useful for programs running as background services.

Example:

import signal
import time

running = True

def terminate(signum, frame):
    global running
    print("Termination request received.")
    running = False

signal.signal(signal.SIGTERM, terminate)

while running:
    print("Working...")
    time.sleep(1)

print("Program stopped safely.")

Here, the handler does not immediately terminate the program. Instead, it changes the value of running to False.

The main loop then finishes naturally, allowing the program to perform any required cleanup before exiting.

5. Using Signals for Graceful Shutdown

One of the most useful applications of signal handling is graceful shutdown.

Suppose a program has opened files, established network connections, or created temporary resources. Abrupt termination could leave those resources in an undesirable state.

A signal handler can notify the main program that it should stop.

For example:

import signal
import time

shutdown_requested = False

def shutdown(signum, frame):
    global shutdown_requested
    shutdown_requested = True
    print("Shutdown requested.")

signal.signal(signal.SIGINT, shutdown)

while not shutdown_requested:
    print("Processing data...")
    time.sleep(1)

print("Cleaning up resources...")
print("Shutdown complete.")

When Ctrl+C is pressed, the program receives SIGINT, sets the shutdown flag, exits the processing loop, and performs cleanup.

This approach is preferable to placing a large amount of cleanup logic directly inside the signal handler.

6. Signal Handling with SIGALRM

The SIGALRM signal can be used to generate an alarm after a specified number of seconds.

Example:

import signal
import time

def alarm_handler(signum, frame):
    print("Time limit reached.")

signal.signal(signal.SIGALRM, alarm_handler)

signal.alarm(5)

print("Waiting for the alarm...")

while True:
    time.sleep(1)

The statement:

signal.alarm(5)

requests an alarm after five seconds.

When the alarm expires, the operating system sends SIGALRM, causing the registered handler to execute.

This mechanism can be useful for certain timeout-related tasks on systems that support SIGALRM.

7. Ignoring a Signal

Python can also be instructed to ignore a particular signal.

For example:

import signal

signal.signal(signal.SIGINT, signal.SIG_IGN)

while True:
    print("Running...")

Here, SIGINT is associated with signal.SIG_IGN, which tells the program to ignore the signal.

This means pressing Ctrl+C will not produce the normal interruption behavior.

Ignoring signals should be done carefully because it can make a program difficult to stop.

8. Restoring the Default Behavior

Python provides signal.SIG_DFL to restore the default handling behavior for a signal.

Example:

import signal

signal.signal(signal.SIGINT, signal.SIG_DFL)

After this assignment, SIGINT is handled according to the operating system's default behavior.

This can be useful when a program temporarily changes signal handling and later wants to restore normal behavior.

9. Important Restrictions

Signal handling has several important restrictions in Python.

In general, Python signal handlers are executed in the main thread of the main Python interpreter. A signal handler should therefore not be treated as an ordinary multithreaded callback.

Signal handlers should also be kept relatively simple. Complicated operations inside a handler can make programs harder to reason about and can introduce unexpected behavior.

A common design is to let the handler set a flag or communicate that shutdown has been requested, while the main program performs the actual work.

10. Signal Handling and KeyboardInterrupt

Python already provides default behavior for SIGINT. When a user presses Ctrl+C, Python normally raises a KeyboardInterrupt exception.

For example:

try:
    while True:
        print("Running...")
except KeyboardInterrupt:
    print("Program interrupted by user.")

This is often sufficient when the only requirement is to handle a user's Ctrl+C interruption.

The signal module becomes more useful when a program needs explicit control over different operating-system signals or needs to coordinate a controlled shutdown.

11. Practical Applications

Signal handling is particularly useful in long-running programs and server applications.

Common applications include:

  1. Gracefully stopping web servers.

  2. Closing files and database connections before shutdown.

  3. Stopping background processing safely.

  4. Responding to process termination requests.

  5. Implementing certain timeout mechanisms.

  6. Reloading configuration in applications that support this behavior.

  7. Coordinating shutdown between a parent process and its workers.

  8. Handling user interruptions in command-line applications.

12. Example of a Complete Graceful Shutdown

A more practical example combines several ideas:

import signal
import time

running = True

def handle_shutdown(signum, frame):
    global running
    print("\nShutdown signal received.")
    running = False

signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)

try:
    while running:
        print("Application is working...")
        time.sleep(2)

finally:
    print("Performing cleanup...")
    print("Application stopped.")

This program responds to both SIGINT and SIGTERM.

When either signal is received, the handler changes running to False. The loop then ends, and the finally block performs the cleanup operation.

Conclusion

Python's signal module provides a mechanism for responding to operating-system signals. It is especially useful for long-running applications that need to react to interruptions or termination requests in a controlled way. Signals such as SIGINT, SIGTERM, and SIGALRM can be associated with custom handler functions using signal.signal().

The most important principle is to keep signal handlers simple. Rather than performing extensive operations directly inside a handler, a good design is to notify the main program and allow it to perform cleanup or shutdown in an orderly manner. This makes applications more reliable, maintainable, and easier to control.