689

I'm working on a python script that starts several processes and database connections. Every now and then I want to kill the script with a Ctrl+C signal, and I'd like to do some cleanup.

In Perl I'd do this:

$SIG{'INT'} = 'exit_gracefully';

sub exit_gracefully {
    print "Caught ^C \n";
    exit (0);
}

How do I do the analogue of this in Python?

Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
James Thompson
  • 46,512
  • 18
  • 65
  • 82

13 Answers13

1018

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.  

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
Matt J
  • 43,589
  • 7
  • 49
  • 57
  • 21
    Could you tell me why to use this in stead of a KeyboardInterrupt exception? Isn't that more intuitive to use? – noio Jun 10 '11 at 14:07
  • 59
    Noio: 2 reasons. First, SIGINT can be sent to your process any number of ways (e.g., 'kill -s INT '); I'm not sure if KeyboardInterruptException is implemented as a SIGINT handler or if it really only catches Ctrl+C presses, but either way, using a signal handler makes your intent explicit (at least, if your intent is the same as OP's). More importantly though, with a signal you don't have to wrap try-catches around everything to make them work, which can be more or less of a composability and general software engineering win depending on the structure of your application. – Matt J Jun 20 '11 at 07:55
  • 54
    Example of why you want to trap the signal instead of catch the Exception. Say you run your program and redirect the output to a log file, `./program.py > output.log`. When you press **Ctrl-C** you want your program to exit gracefully by having it log that all data files have been flushed and marked clean to confirm they are left in a known good state. But Ctrl-C sends SIGINT to all processes in a pipeline, so the shell may close STDOUT (now "output.log") before program.py finishes printing the final log. Python will complain, "close failed in file object destructor: Error in sys.excepthook:". – Noah Spurrier Jul 03 '11 at 22:07
  • 27
    Note that signal.pause() is unavailable on Windows. http://docs.python.org/dev/library/signal.html – May Oakes Aug 03 '11 at 16:30
  • 1
    this works fine for this trivial case, but I find when the main routine is using urllib2.urlopen, the sys.exit() in the interrupt handler is being ignored. still trying to figure out why. – jcomeau_ictx Aug 28 '12 at 07:03
  • 1
    @jcomeau_ictx I have the same bug when the main routine is sleeping (`time.sleep`). I really wonder why that is... – MiniQuark Mar 06 '13 at 10:54
  • 1
    @jcomeau_ictx Probably urllib2 is catching exceptions with "except:" which also catches SystemExit. Not much you can do about it in this case but patching urllib2 to fix their bug. – MarioVilas Mar 13 '13 at 13:12
  • 16
    -1 unicorns for using signal.pause(), suggests that I would have to wait at such a blocking call instead of doing some real work. ;) – Nick T Aug 03 '14 at 15:49
  • cosmetic detail: signal arg of signal_handler() colides with signal module – dim Jul 01 '15 at 18:33
  • 6
    FYI, in python 3.4 `KeyboardInterrupt` *is thrown* when the process receives a SIGINT, e.g. `kill -s SIGINT `. Though I agree that this is not obvious from the name, and a signal handler has clearer intent. – Mark E. Haase May 17 '16 at 13:42
  • 1
    why are you shadowing signal? – waspinator Feb 02 '17 at 21:44
  • Run the code and got this error on Windows ` signal.pause() AttributeError: module 'signal' has no attribute 'pause'` –  Nov 23 '18 at 08:36
  • 1
    @MattJ Module signal has no attribute 'pause'. At least not in Python 3.5 for Windows. –  Feb 14 '19 at 13:04
  • 1
    @Sabrina You can replace it by an infinite loop like `while True...continue...` just for testing...or perhaps doing some real work... –  Feb 14 '19 at 13:22
  • If needed to kill from thread you can also use os._exit(130) – Edoardo Vignati Feb 20 '20 at 11:03
  • 1
    Note that at the time of this comment, you have to get the correct function's signature : you HAVE TO put the two args `(sig, frame)` even if you do not use these. The function's name does not matter. – NoxFly Jun 13 '22 at 15:58
209

You can treat it like an exception (KeyboardInterrupt), like any other. Make a new file and run it from your shell with the following contents to see what I mean:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()
Neuron
  • 5,141
  • 5
  • 38
  • 59
rledley
  • 2,325
  • 1
  • 12
  • 7
  • 32
    Attention when using this solution. You should use also this code before KeyboardInterrupt catch block: `signal.signal(signal.SIGINT, signal.default_int_handler)`, or you're going to fail, because KeyboardInterrupt does not fire in every situation in which it should fire! Details are [here](http://stackoverflow.com/a/40785230/1908192). – Velda Nov 24 '16 at 15:23
  • this wont work for strg+d? even with the signal codeline before – Asara Jul 05 '20 at 11:50
  • The problem with KeyboardInterrupt is, it'll move you out of the try block. It may not be graceful all the time. Think about long running loop. `signal.signal` gives you more control. – Shiplu Mokaddim Jun 16 '22 at 09:08
81

And as a context manager:

import signal

class GracefulInterruptHandler(object):

    def __init__(self, sig=signal.SIGINT):
        self.sig = sig

    def __enter__(self):

        self.interrupted = False
        self.released = False

        self.original_handler = signal.getsignal(self.sig)

        def handler(signum, frame):
            self.release()
            self.interrupted = True

        signal.signal(self.sig, handler)

        return self

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):

        if self.released:
            return False

        signal.signal(self.sig, self.original_handler)

        self.released = True

        return True

To use:

with GracefulInterruptHandler() as h:
    for i in xrange(1000):
        print "..."
        time.sleep(1)
        if h.interrupted:
            print "interrupted!"
            time.sleep(2)
            break

Nested handlers:

with GracefulInterruptHandler() as h1:
    while True:
        print "(1)..."
        time.sleep(1)
        with GracefulInterruptHandler() as h2:
            while True:
                print "\t(2)..."
                time.sleep(1)
                if h2.interrupted:
                    print "\t(2) interrupted!"
                    time.sleep(2)
                    break
        if h1.interrupted:
            print "(1) interrupted!"
            time.sleep(2)
            break

From here: https://gist.github.com/2907502

Udi
  • 29,222
  • 9
  • 96
  • 129
  • It could also throw a `StopIteration` to break the innermost loop when a ctrl-C is pressed, right? – Theo Belaire Feb 05 '14 at 01:28
  • @TheoBelaire Instead of just throwing a StopIteration, I would create a generator that accepts an iterable as a parameter and registers/releases the signal handler. – Udi Sep 18 '14 at 08:27
33

You can handle CTRL+C by catching the KeyboardInterrupt exception. You can implement any clean-up code in the exception handler.

Mwiza
  • 7,780
  • 3
  • 46
  • 42
Jay Conrod
  • 28,943
  • 19
  • 98
  • 110
32

Yet Another Snippet

Referred main as the main function and exit_gracefully as the Ctrl+C handler

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()
Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Jossef Harush Kadouri
  • 32,361
  • 10
  • 130
  • 129
  • 8
    You should only use except for stuff that isn't supposed to happen. In this case KeyboardInterrupt is supposed to happen. So this is not a good construction. – Tristan Jul 29 '16 at 15:20
  • 26
    @TristanT In any other language yes, but in Python exceptions are not just for things that are not supposed to happen. It is actually considered good style in Python to use exceptions for flow control (where appropriate). – Ian Goldby Nov 29 '17 at 13:57
  • 2
    why not just `exit_gracefully()` in `except` instead of passing and adding `finally`? – Break Jan 27 '21 at 04:24
  • 6
    @Break its in finally so `exit_gracefully()` will also be called after `main()` finish – Jossef Harush Kadouri Jan 27 '21 at 08:39
30

From Python's documentation:

import signal
import time

def handler(signum, frame):
    print 'Here you go'

signal.signal(signal.SIGINT, handler)

time.sleep(10) # Press Ctrl+c here
pradyunsg
  • 18,287
  • 11
  • 43
  • 96
sunqiang
  • 6,422
  • 1
  • 32
  • 32
12

I adapted the code from @udi to support multiple signals (nothing fancy) :

class GracefulInterruptHandler(object):
    def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
        self.signals = signals
        self.original_handlers = {}

    def __enter__(self):
        self.interrupted = False
        self.released = False

        for sig in self.signals:
            self.original_handlers[sig] = signal.getsignal(sig)
            signal.signal(sig, self.handler)

        return self

    def handler(self, signum, frame):
        self.release()
        self.interrupted = True

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):
        if self.released:
            return False

        for sig in self.signals:
            signal.signal(sig, self.original_handlers[sig])

        self.released = True
        return True

This code support the keyboard interrupt call (SIGINT) and the SIGTERM (kill <process>)

Cyril N.
  • 38,875
  • 36
  • 142
  • 243
  • Did you try this with SIGTERM? It appears that the original handler for SIGTERM is not callable. – 1' OR 1 -- Sep 19 '21 at 23:18
  • I've added a print comment in the handler, and got the event for both SIGINT and SIGTERM. Adding a print to the condition (`if h.interupted:`) was shown too. So I think it works yes. – Cyril N. Sep 21 '21 at 06:16
12

If you want to ensure that your cleanup process finishes I would add on to Matt J's answer by using a SIG_IGN so that further SIGINT are ignored which will prevent your cleanup from being interrupted.

import signal
import sys

def signal_handler(signum, frame):
    signal.signal(signum, signal.SIG_IGN) # ignore additional signals
    cleanup() # give your process a chance to clean up
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler) # register the signal with the signal handler first
do_stuff()
Josh Correia
  • 3,807
  • 3
  • 33
  • 50
10

In contrast to Matt J his answer, I use a simple object. This gives me the possibily to parse this handler to all the threads that needs to be stopped securlery.

class SIGINT_handler():
    def __init__(self):
        self.SIGINT = False

    def signal_handler(self, signal, frame):
        print('You pressed Ctrl+C!')
        self.SIGINT = True


handler = SIGINT_handler()
signal.signal(signal.SIGINT, handler.signal_handler)

Elsewhere

while True:
    # task
    if handler.SIGINT:
        break
Community
  • 1
  • 1
Thomas Devoogdt
  • 816
  • 11
  • 16
  • You should use an event or `time.sleep()` instead of doing a busy loop on a variable. – OlivierM May 30 '19 at 07:43
  • 1
    @OlivierM This is really application specific and definitely not the point of this example. For example, blocking calls or await functions won't keep the CPU busy. Furthermore, this is just an example of how things can be done. KeyboardInterrupts are often enough, like mentioned in other answers. – Thomas Devoogdt May 31 '19 at 13:28
  • @ThomasDevoogdt I love the elegance of this solution -- I can completely understand what's going on here. Can you comment on why this is superior (or inferior) to the other solutions? In particular, I find Udi/Cyril N.'s solutions to be very complete, but much more complex. – Aaron Ciuffo Oct 23 '20 at 19:58
  • I don't like to call this solution superior over another. A good developer looks at different answers and distills its own application-specific solution out of the available suggestions. But to give one advantage, it's pretty simple to use and implement. A rather big disadvantage is the need for a non-blocking loop in the thread that is handled. – Thomas Devoogdt Oct 25 '20 at 13:40
4

You can use the functions in Python's built-in signal module to set up signal handlers in python. Specifically the signal.signal(signalnum, handler) function is used to register the handler function for signal signalnum.

Brandon E Taylor
  • 24,881
  • 6
  • 47
  • 71
3

thanks for existing answers, but added signal.getsignal()

import signal

# store default handler of signal.SIGINT
default_handler = signal.getsignal(signal.SIGINT)
catch_count = 0

def handler(signum, frame):
    global default_handler, catch_count
    catch_count += 1
    print ('wait:', catch_count)
    if catch_count > 3:
        # recover handler for signal.SIGINT
        signal.signal(signal.SIGINT, default_handler)
        print('expecting KeyboardInterrupt')

signal.signal(signal.SIGINT, handler)
print('Press Ctrl+c here')

while True:
    pass
gsw945
  • 88
  • 2
  • 6
0

Personally, I couldn't use try/except KeyboardInterrupt because I was using standard socket (IPC) mode which is blocking. So the SIGINT was cueued, but came only after receiving data on the socket.

Setting a signal handler behaves the same.

On the other hand, this only works for an actual terminal. Other starting environments might not accept Ctrl+C, or pre-handle the signal.

Also, there are "Exceptions" and "BaseExceptions" in Python, which differ in the sense that interpreter needs to exit cleanly itself, so some exceptions have a higher priority than others (Exceptions is derived from BaseException)

Misha Akopov
  • 12,241
  • 27
  • 68
  • 82
Ate Somebits
  • 287
  • 1
  • 18
0

Here's a one-liner (minus the imports), tested with Python 3.10 (not sure about other versions):

#!/usr/bin/env python3
from signal import SIGINT, SIGTERM, sigwait


print('received', sigwait({SIGINT, SIGTERM}))

Though the behavior is funny if you send another signal, like USR1, before INT/TERM. sigwait will wait for INT/TERM, but won't output the "received {signal}" line as expected.

$ ./test.py # ctrl + c
^Creceived Signals.SIGINT

$ ./test.py # kill -TERM <pid>
received Signals.SIGTERM

$ ./test.py # kill -USR1 <pid> ; kill -TERM <pid>
User defined signal 1: 30
jeffwtribble
  • 1,131
  • 1
  • 9
  • 6