How to Capture SIGINT in Python
SIGINT (Signal Interrupt) is the signal sent to a program when a user presses Ctrl+C (or Ctrl+F2 on some systems). By default, this terminates the program immediately. However, Python allows you to capture this signal and execute custom cleanup logic, such as saving data, closing connections, or printing a graceful shutdown message, before the program exits.
This guide covers multiple ways to handle SIGINT in Python, from simple try/except blocks to the more powerful signal module.
Using try/except KeyboardInterrupt (Simplest)โ
The easiest way to capture Ctrl+C is wrapping your code in a try/except block that catches KeyboardInterrupt:
import sys
from time import sleep
counter = 1
while True:
try:
print(f"Running... count: {counter}")
sleep(0.5)
counter += 1
except KeyboardInterrupt:
print("\nโ
Program stopped gracefully by user.")
sys.exit(0)
Output (after pressing Ctrl+C):
Running... count: 1
Running... count: 2
Running... count: 3
^C
โ
Program stopped gracefully by user.
This approach is straightforward and works well for simple scripts. The KeyboardInterrupt exception is raised whenever the user presses Ctrl+C.
Using the signal Moduleโ
For more control over signal handling, Python's signal module lets you register a custom handler function that runs when SIGINT is received. This is the standard approach for production applications.
import signal
import sys
from time import sleep
def handle_sigint(signum, frame):
"""Custom handler for SIGINT (Ctrl+C)."""
print(f"\nโ
Received signal {signum} (SIGINT)")
print("Performing cleanup before exit...")
sys.exit(0)
# Register the custom signal handler
signal.signal(signal.SIGINT, handle_sigint)
print("Program is running. Press Ctrl+C to stop.")
counter = 1
while True:
print(f"Working... iteration {counter}")
sleep(1)
counter += 1
Output (after pressing Ctrl+C):
Program is running. Press Ctrl+C to stop.
Working... iteration 1
Working... iteration 2
Working... iteration 3
^C
โ
Received signal 2 (SIGINT)
Performing cleanup before exit...
Understanding the Handler Parametersโ
The signal handler function receives two arguments:
| Parameter | Description |
|---|---|
signum | The signal number (2 for SIGINT) |
frame | The current stack frame (useful for debugging) |
import signal
import sys
def handler(signum, frame):
print(f"\nSignal number: {signum}")
print(f"Interrupted at: {frame.f_code.co_filename}, line {frame.f_lineno}")
sys.exit(0)
signal.signal(signal.SIGINT, handler)
Practical Example: Graceful Shutdown with Cleanupโ
A real-world use case is performing cleanup operations before shutting down, i.e. saving files, closing database connections, or flushing logs:
import signal
import sys
from time import sleep
class DataProcessor:
def __init__(self):
self.data = []
self.running = True
def process(self):
"""Simulate data processing."""
counter = 0
while self.running:
counter += 1
self.data.append(f"record_{counter}")
print(f"Processed record {counter}")
sleep(0.5)
def cleanup(self):
"""Save data and release resources."""
print(f"\n๐ฆ Saving {len(self.data)} records...")
# Simulate saving to file
with open("output.txt", "w") as f:
f.write("\n".join(self.data))
print("โ
Data saved to output.txt")
print("๐ Closing connections...")
print("๐ Shutdown complete.")
def stop(self):
"""Signal the processor to stop."""
self.running = False
processor = DataProcessor()
def handle_shutdown(signum, frame):
processor.stop()
processor.cleanup()
sys.exit(0)
signal.signal(signal.SIGINT, handle_shutdown)
print("Data processor started. Press Ctrl+C to stop.\n")
processor.process()
Output (after pressing Ctrl+C):
Data processor started. Press Ctrl+C to stop.
Processed record 1
Processed record 2
Processed record 3
^C
๐ฆ Saving 3 records...
โ
Data saved to output.txt
๐ Closing connections...
๐ Shutdown complete.
Using a Flag for Controlled Shutdownโ
Instead of calling sys.exit() immediately, you can set a flag that allows the main loop to finish its current iteration gracefully:
import signal
from time import sleep
shutdown_requested = False
def handle_sigint(signum, frame):
global shutdown_requested
print("\nโ ๏ธ Shutdown requested. Finishing current task...")
shutdown_requested = True
signal.signal(signal.SIGINT, handle_sigint)
print("Processing tasks. Press Ctrl+C to stop after current task.\n")
for i in range(1, 100):
if shutdown_requested:
print(f"โ
Stopped after completing task {i - 1}")
break
print(f"Task {i}: processing...")
sleep(1) # Simulate work
print(f"Task {i}: complete")
Output (pressing Ctrl+C during task 3):
Processing tasks. Press Ctrl+C to stop after current task.
Task 1: processing...
Task 1: complete
Task 2: processing...
Task 2: complete
Task 3: processing...
^C
โ ๏ธ Shutdown requested. Finishing current task...
Task 3: complete
โ
Stopped after completing task 3
The flag-based approach ensures that the current operation completes before shutting down, preventing data corruption or partial writes.
Handling Multiple Ctrl+C Pressesโ
Sometimes users press Ctrl+C multiple times. You can handle this by counting signals or forcing exit on the second press:
import signal
import sys
from time import sleep
sigint_count = 0
def handle_sigint(signum, frame):
global sigint_count
sigint_count += 1
if sigint_count == 1:
print("\nโ ๏ธ Press Ctrl+C again to force exit.")
print(" Attempting graceful shutdown...")
else:
print("\nโ Force exit!")
sys.exit(1)
signal.signal(signal.SIGINT, handle_sigint)
print("Running... Press Ctrl+C to stop.\n")
while sigint_count == 0:
sleep(0.5)
# Graceful shutdown
print("๐งน Cleaning up...")
sleep(2)
print("โ
Done.")
Restoring the Default Handlerโ
You can restore the default SIGINT behavior (immediate termination) by setting the handler back to signal.SIG_DFL:
import signal
# Custom handler
signal.signal(signal.SIGINT, lambda s, f: print("\nIgnored!"))
# Restore default behavior
signal.signal(signal.SIGINT, signal.SIG_DFL)
Quick Referenceโ
| Approach | Best For |
|---|---|
try/except KeyboardInterrupt | Simple scripts, quick handling |
signal.signal(signal.SIGINT, handler) | Production apps, custom cleanup |
| Flag-based shutdown | Completing current work before exit |
| Double Ctrl+C pattern | User-friendly forced exit option |
Common Signals Referenceโ
| Signal | Number | Trigger | Default Action |
|---|---|---|---|
SIGINT | 2 | Ctrl+C | Terminate |
SIGTERM | 15 | kill command | Terminate |
SIGHUP | 1 | Terminal closed | Terminate |
SIGALRM | 14 | alarm() timer | Terminate |
On Windows, only SIGINT, SIGTERM, SIGABRT, and SIGBREAK are supported. Linux and macOS support the full range of POSIX signals.
Conclusionโ
Capturing SIGINT in Python lets you handle Ctrl+C gracefully instead of abruptly terminating your program. For simple scripts, a try/except KeyboardInterrupt block is sufficient. For production applications that need cleanup operations, resource management, or controlled shutdown, use the signal module to register a custom handler. The flag-based approach is ideal when you need to ensure the current operation completes before shutting down. Whichever method you choose, graceful signal handling makes your Python programs more robust and user-friendly.