How do I capture SIGINT in Python?
Asked Answered
M

13

708

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?

Mercurous answered 10/7, 2009 at 22:49 Comment(0)
S
1045

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.  

Siobhan answered 10/7, 2009 at 22:52 Comment(16)
Could you tell me why to use this in stead of a KeyboardInterrupt exception? Isn't that more intuitive to use?Ayers
Noio: 2 reasons. First, SIGINT can be sent to your process any number of ways (e.g., 'kill -s INT <pid>'); 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.Siobhan
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:".Midterm
Note that signal.pause() is unavailable on Windows. docs.python.org/dev/library/signal.htmlCannabis
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.Arleta
@Arleta I have the same bug when the main routine is sleeping (time.sleep). I really wonder why that is...Arbitress
@Arleta 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.Dummy
-1 unicorns for using signal.pause(), suggests that I would have to wait at such a blocking call instead of doing some real work. ;)Nieman
cosmetic detail: signal arg of signal_handler() colides with signal moduleRatoon
FYI, in python 3.4 KeyboardInterrupt is thrown when the process receives a SIGINT, e.g. kill -s SIGINT <pid>. Though I agree that this is not obvious from the name, and a signal handler has clearer intent.Emit
why are you shadowing signal?Spiniferous
Run the code and got this error on Windows ` signal.pause() AttributeError: module 'signal' has no attribute 'pause'`Gielgud
@MattJ Module signal has no attribute 'pause'. At least not in Python 3.5 for Windows.Brynne
@Sabrina You can replace it by an infinite loop like while True...continue... just for testing...or perhaps doing some real work...Brynne
If needed to kill from thread you can also use os._exit(130)Bagehot
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.Ocarina
S
213

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()
Stamper answered 10/7, 2009 at 22:54 Comment(3)
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.Chesterchesterfield
this wont work for strg+d? even with the signal codeline beforeMontparnasse
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.Subclavius
M
82

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

Mabel answered 10/6, 2012 at 22:23 Comment(2)
It could also throw a StopIteration to break the innermost loop when a ctrl-C is pressed, right?Schou
@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.Mabel
I
33

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

Intestate answered 10/7, 2009 at 22:52 Comment(0)
K
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()
Klingensmith answered 15/8, 2015 at 15:0 Comment(4)
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.Baroscope
@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).Stenosis
why not just exit_gracefully() in except instead of passing and adding finally?Alyce
@Alyce its in finally so exit_gracefully() will also be called after main() finishKlingensmith
L
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
Lactescent answered 10/7, 2009 at 22:56 Comment(0)
P
14

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()
Posey answered 30/1, 2020 at 18:32 Comment(1)
Thx. This is the sensible way of doing it, by clearly registering the signal handler.Omalley
A
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>)

Adventurous answered 4/3, 2016 at 14:27 Comment(2)
Did you try this with SIGTERM? It appears that the original handler for SIGTERM is not callable.Navvy
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.Adventurous
C
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
Caspian answered 4/5, 2017 at 15:52 Comment(4)
You should use an event or time.sleep() instead of doing a busy loop on a variable.Parable
@Parable 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.Caspian
@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.Aldas
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.Caspian
C
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.

Causality answered 10/7, 2009 at 22:54 Comment(0)
R
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
Reservist answered 31/12, 2018 at 5:50 Comment(1)
An alternative to signal.getsignal is to save the return value of signal.signal, which is the same thing.Geiss
B
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)

Bitterweed answered 10/12, 2018 at 18:1 Comment(0)
A
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
Augustinaaugustine answered 21/3, 2023 at 19:29 Comment(2)
This is weird. You don't have to register the signal handler?Omalley
@Omalley sigwait blocks the current thread (in this case, the main thread) until one of the signals you've provided is received: docs.python.org/3/library/signal.html#signal.sigwaitAugustinaaugustine

© 2022 - 2024 — McMap. All rights reserved.