Register your handler with signal.signal like this:

#!/usr/bin/env python
import signal
import sys

def signal_handler(sig, frame):
    print('You pressed Ctrl+C!')
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
signal.pause()

Code adapted from here.

More documentation on signal can be found here.  

Answer from Matt J on Stack Overflow
🌐
Python
docs.python.org › 3 › library › signal.html
signal — Set handlers for asynchronous events
Interrupt from keyboard (CTRL + C). Default action is to raise KeyboardInterrupt. ... Kill signal.
Discussions

Signal handler is registered, but process still exits abruptly on Ctrl+C (implementing graceful shutdown)
According to the docs, Asyncio.run() registers SIGINT handler manually in order to not hang program. https://docs.python.org/3/library/asyncio-runner.html#asyncio.run Here is relevant fragment: When signal.SIGINT is raised by Ctrl-C, KeyboardInterrupt exception is raised in the main thread by default. However this doesn’t work with asyncio because it can interrupt asyncio internals and can hang the program from exiting. To mitigate this issue, asyncio handles signal.SIGINT as follows: asyncio.Runner.run() installs a custom signal.SIGINT handler before any user code is executed and removes it when exiting from the function. The Runner creates the main task for the passed coroutine for its execution. When signal.SIGINT is raised by Ctrl-C, the custom signal handler cancels the main task by calling asyncio.Task.cancel() which raises asyncio.CancelledError inside the main task. This causes the Python stack to unwind, try/except and try/finally blocks can be used for resource cleanup. After the main task is cancelled, asyncio.Runner.run() raises KeyboardInterrupt . A user could write a tight loop which cannot be interrupted by asyncio.Task.cancel() , in which case the second following Ctrl-C immediately raises the KeyboardInterrupt without cancelling the main task. So if you want to make your code resistant to sigint, you have to clear any signal handlers at the begining of your job: https://stackoverflow.com/questions/22916783/reset-python-sigint-to-default-signal-handler But that might be a little overcomplicated. It would be much easier to implement a job proccessor as a separate thread, without asyncio interference. More on reddit.com
🌐 r/learnpython
4
6
September 4, 2024
signals - Python: Catch Ctrl-C command. Prompt "really want to quit (y/n)", resume execution if no - Stack Overflow
The code restores the original signal handler for the duration of raw_input; raw_input itself is not re-entrable, and re-entering it will lead to RuntimeError: can't re-enter readline being raised from time.sleep which is something we don't want as it is harder to catch than KeyboardInterrupt. Rather, we let 2 consecutive Ctrl... More on stackoverflow.com
🌐 stackoverflow.com
September 5, 2019
Ctrl-C doesn't send SIGINT to python program launched with debugger
Extensive searching leads me to believe this probably isn't a bug, but I still can't figure out how to get the expected behavior in Code. In a nutshell, "Ctrl-C" should raise a Ke... More on github.com
🌐 github.com
13
December 14, 2020
Invoke function before closing script (Ctrl + C)
Blender Artists is an online creative forum that is dedicated to the growth and education of the 3D software Blender. More on blenderartists.org
🌐 blenderartists.org
1
0
September 29, 2022
🌐
Xanthium
xanthium.in › operating-system-signal-handling-in-python3
Capturing and Handling OS signals like SIGINT (CTRL-C) in Python | xanthium enterprises
October 18, 2023 - ... import signal import time Sentry = True # Create a Signal Handler for Signals.SIGINT: CTRL + C def SignalHandler_SIGINT(SignalNumber,Frame): global Sentry Sentry = False signal.signal(signal.SIGINT,SignalHandler_SIGINT) #regsiter signal with handler while Sentry: #exit loop when Sentry ...
🌐
Reddit
reddit.com › r/learnpython › signal handler is registered, but process still exits abruptly on ctrl+c (implementing graceful shutdown)
r/learnpython on Reddit: Signal handler is registered, but process still exits abruptly on Ctrl+C (implementing graceful shutdown)
September 4, 2024 -

Hello, I have a job processor written in Python. It's a loop that pulls a job from a database, does stuff with it inside a transaction with row-level locking, then writes the result back to the database. Jobs are relatively small, usually shorter than 5s.

import asyncio
import signal

running = True

def signal_handler(sig, frame):
    global running
    print("SIGINT received, stopping on next occasion")
    running = False

signal.signal(signal.SIGINT, signal_handler)
while running:
    asyncio.run(do_one_job()) # imported from a module

I would expect the above code to work. But when Ctrl+Cing the process, the current job stops abruptly with a big stack trace and an exception from one of the libraries used indirectly from do_one_job (urllib.ProtocolError: Connection aborted). The whole point of my signal handling is to avoid interrupting a job while it's running. While jobs are processed within transactions and shouldn't break the DB's consistency, I'd rather have an additional layer of safety by trying to wait until they are properly finished, especially since they're short.

Why can do_one_job() observe a signal that's supposed to be already handled? How can I implement graceful shutdown in Python?

Top answer
1 of 2
3
According to the docs, Asyncio.run() registers SIGINT handler manually in order to not hang program. https://docs.python.org/3/library/asyncio-runner.html#asyncio.run Here is relevant fragment: When signal.SIGINT is raised by Ctrl-C, KeyboardInterrupt exception is raised in the main thread by default. However this doesn’t work with asyncio because it can interrupt asyncio internals and can hang the program from exiting. To mitigate this issue, asyncio handles signal.SIGINT as follows: asyncio.Runner.run() installs a custom signal.SIGINT handler before any user code is executed and removes it when exiting from the function. The Runner creates the main task for the passed coroutine for its execution. When signal.SIGINT is raised by Ctrl-C, the custom signal handler cancels the main task by calling asyncio.Task.cancel() which raises asyncio.CancelledError inside the main task. This causes the Python stack to unwind, try/except and try/finally blocks can be used for resource cleanup. After the main task is cancelled, asyncio.Runner.run() raises KeyboardInterrupt . A user could write a tight loop which cannot be interrupted by asyncio.Task.cancel() , in which case the second following Ctrl-C immediately raises the KeyboardInterrupt without cancelling the main task. So if you want to make your code resistant to sigint, you have to clear any signal handlers at the begining of your job: https://stackoverflow.com/questions/22916783/reset-python-sigint-to-default-signal-handler But that might be a little overcomplicated. It would be much easier to implement a job proccessor as a separate thread, without asyncio interference.
2 of 2
2
I can't reproduce using simple code. The following works correctly for me: import asyncio import signal from random import randint running = True def signal_handler(sig, frame): global running print("SIGINT received, stopping on next occasion") running = False async def do_one_job(): print("Starting a job", end="", flush=True) for _ in range(randint(5, 25)): print(".", end="", flush=True) await asyncio.sleep(0.25) print("Done") signal.signal(signal.SIGINT, signal_handler) while running: asyncio.run(do_one_job()) (Although I don't think it's good to call asyncio.run over and over again. I would rewrite such that it's called once on a main() and that the while loop repeats inside that with await do_one_job()). But still, it works for me. There must be something rewriting the signal handling beyond asyncio, or perhaps try different versions of Python?
🌐
Instructables
instructables.com › circuits › raspberry pi
Learn How to Capture and Handle OS Signals Like SIGINT (CTRL-C) in Python for Beginners : 6 Steps - Instructables
November 24, 2022 - Learn How to Capture and Handle OS Signals Like SIGINT (CTRL-C) in Python for Beginners: Here we will learn how to capture and handle operating system signals like SIGINT and SIGBREAK on Linux and Windows OS's to control the flow of your python script during execution.
🌐
JBI Training
jbinternational.co.uk › article › view › 3649
How to Send Ctrl-C to Interrupt and Stop a Python Script
... import signal def recursive_func(): ... the creation of scripts that are responsive and resilient. Ctrl-C sends a SIGINT signal that raises a KeyboardInterrupt in Python......
🌐
Code-maven
python.code-maven.com › catch-control-c-in-python
Signal handling: Catch Ctrl-C in Python
import signal import time def handler(signum, frame): res = input("Ctrl-c was pressed. Do you really want to exit? y/n ") if res == 'y': exit(1) signal.signal(signal.SIGINT, handler) count = 0 while True: print(count) count += 1 time.sleep(0.1) This is how it looks like when the numbers are running out of the screen: ... Gabor Szabo, the author of the Python Maven web site.
Find elsewhere
🌐
DevDungeon
devdungeon.com › content › python-catch-sigint-ctrl-c
Python Catch SIGINT (CTRL-C) | DevDungeon
May 26, 2019 - Exiting gracefully') exit(0) if __name__ == '__main__': # Tell Python to run the handler() function when SIGINT is recieved signal(SIGINT, handler) print('Running. Press CTRL-C to exit.') while True: # Do nothing and hog CPU forever until SIGINT ...
Top answer
1 of 4
79

The python signal handlers do not seem to be real signal handlers; that is they happen after the fact, in the normal flow and after the C handler has already returned. Thus you'd try to put your quit logic within the signal handler. As the signal handler runs in the main thread, it will block execution there too.

Something like this seems to work nicely.

import signal
import time
import sys

def run_program():
    while True:
        time.sleep(1)
        print("a")

def exit_gracefully(signum, frame):
    # restore the original signal handler as otherwise evil things will happen
    # in raw_input when CTRL+C is pressed, and our signal handler is not re-entrant
    signal.signal(signal.SIGINT, original_sigint)

    try:
        if raw_input("\nReally quit? (y/n)> ").lower().startswith('y'):
            sys.exit(1)

    except KeyboardInterrupt:
        print("Ok ok, quitting")
        sys.exit(1)

    # restore the exit gracefully handler here    
    signal.signal(signal.SIGINT, exit_gracefully)

if __name__ == '__main__':
    # store the original SIGINT handler
    original_sigint = signal.getsignal(signal.SIGINT)
    signal.signal(signal.SIGINT, exit_gracefully)
    run_program()

The code restores the original signal handler for the duration of raw_input; raw_input itself is not re-entrable, and re-entering it will lead to RuntimeError: can't re-enter readline being raised from time.sleep which is something we don't want as it is harder to catch than KeyboardInterrupt. Rather, we let 2 consecutive Ctrl-C's to raise KeyboardInterrupt.

2 of 4
8

from https://gist.github.com/rtfpessoa/e3b1fe0bbfcd8ac853bf

#!/usr/bin/env python

import signal
import sys

def signal_handler(signal, frame):
  # your code here
  sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

Bye!

🌐
David Hamann
davidhamann.de › posts › handling and confirming (interrupt) signals in python
Handling and confirming (interrupt) signals in Python | David Hamann
September 29, 2022 - A simple way of stopping further calls to the handler is to reset the handler for the signal yet again – this time to ignore interrupts by using the SIG_IGN handler: if ask: ... else: signal.signal(signum, signal.SIG_IGN) Running the program now you will see that the “Cleaning/exiting” part only happens once, even when you keep pressing <ctrl-c>.
🌐
Vorpus
vorpus.org › blog › control-c-handling-in-python-and-trio
Control-C handling in Python and Trio — njs blog
In this post I'll explore some different options for handling control-C, and explain Trio's solution – with a bonus deep dive into signal handling and some rather obscure corners of CPython's guts. The tl;dr is: if you're writing a program using trio, then control-C should generally Just Work the way you expect from regular Python, i.e., it will raise KeyboardInterrupt somewhere in your code, and this exception then propagates out to unwind your stack, run cleanup handlers, and eventually exit the program.
🌐
GeeksforGeeks
geeksforgeeks.org › how-to-capture-sigint-in-python
How to capture SIGINT in Python? - GeeksforGeeks
April 28, 2025 - Also, we define signal.SIGINT, which will perform interruption through the keyboard either by pressing Ctrl+ C or Ctrl+F2. ... Step 4: Later on, display some lines as a message to make the user know what to do for keyboard interruption.
🌐
Super Fast Python
superfastpython.com › home › tutorials › asyncio handle control-c (sigint)
Asyncio Handle Control-C (SIGINT) - Super Fast Python
April 17, 2024 - This is a normal behavior of the main task. When signal.SIGINT is raised by Ctrl-C, the custom signal handler cancels the main task by calling asyncio.Task.cancel() which raises asyncio.CancelledError inside the main task.
🌐
Quora
quora.com › How-does-Python-handle-Ctrl-C
How does Python handle Ctrl C? - Quora
Ctrl+C → SIGINT → Python raises KeyboardInterrupt in the main thread (subject to signal delivery timing). Behavior is influenced by threads, blocking C calls, and event loops; handle KeyboardInterrupt or use signal handlers/coordination ...
🌐
GitHub
github.com › microsoft › debugpy › issues › 500
Ctrl-C doesn't send SIGINT to python program launched with debugger · Issue #500 · microsoft/debugpy
December 14, 2020 - The above works as expected when a program is run from the terminal with python3 program.py, but when launched with the debugger as above, SIGINT isn't passed to Python. Open new folder. ... import time number = 0 try: while True: number += 1 print(number) time.sleep(.5) except KeyboardInterrupt: print("Finished with ctrl-c")
Author   alexwhittemore
🌐
Python
docs.python.org › 3.10 › library › signal.html
signal — Set handlers for asynchronous events — Python 3.10.16 documentation
Interrupt from keyboard (CTRL + C). Default action is to raise KeyboardInterrupt. ... Kill signal.
🌐
Aalvrz
aalvrz.me › posts › gracefully-exiting-python-context-managers-on-ctrl-c
Gracefully Exiting Python Context Managers on Ctrl+C · Andrés Álvarez
April 10, 2020 - When you press Ctrl+C, the handler we registered (_sigint_handler) will be called instead of a KeyboardInterrupt error being raised. In this method we manually call the context manager's __exit__ method, passing a value of None for all 3 arguments. This is what is actually passed when __exit__ ...