138

I'm writing a command line utility in Python which, since it is production code, ought to be able to shut down cleanly without dumping a bunch of stuff (error codes, stack traces, etc.) to the screen. This means I need to catch keyboard interrupts.

I've tried using both a try catch block like:

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print 'Interrupted'
        sys.exit(0)

and catching the signal itself (as in this post):

import signal
import sys

def sigint_handler(signal, frame):
    print 'Interrupted'
    sys.exit(0)
signal.signal(signal.SIGINT, sigint_handler)

Both methods seem to work quite well during normal operation. However, if the interrupt comes during cleanup code at the end of the application, Python seems to always print something to the screen. Catching the interrupt gives

^CInterrupted
Exception KeyboardInterrupt in <bound method MyClass.__del__ of <path.to.MyClass object at 0x802852b90>> ignored

whereas handling the signal gives either

^CInterrupted
Exception SystemExit: 0 in <Finalize object, dead> ignored

or

^CInterrupted
Exception SystemExit: 0 in <bound method MyClass.__del__ of <path.to.MyClass object at 0x802854a90>> ignored

Not only are these errors ugly, they're not very helpful (especially to an end user with no source code)!

The cleanup code for this application is fairly big, so there's a decent chance that this issue will be hit by real users. Is there any way to catch or block this output, or is it just something I'll have to deal with?

Community
  • 1
  • 1
Dan
  • 2,952
  • 4
  • 23
  • 29
  • 2
    Why don't you replace `sys.stdout`/`sys.stderr`? Like `sys.stderr = open(os.devnull, 'w')`?. If you really don't care about the final output then this seems like the obvious solution. – Bakuriu Jan 14 '14 at 18:33
  • 2
    There is an `os._exit` but it looks like nasal demons to me. Where is your cleanup code, are you using [atexit](http://docs.python.org/2/library/atexit.html#module-atexit) module for that? – wim Jan 14 '14 at 18:36
  • 1
    @Bakuriu: While redirecting stderr will quiet the output, it also squashes legitimate errors that the user can do something about, like file-not-found or host-unreachable. – Dan Jan 14 '14 at 18:52
  • 4
    @Dan It doesn't have to be `/dev/null`. You can write a custom file-like object that hides only messages with a given format. – Bakuriu Jan 14 '14 at 19:18
  • 2
    @Bakuriu: Still seems pretty hacky to me. I'll do that if I have to, but I feel like this is something that ought to be built into the language. – Dan Jan 14 '14 at 21:14
  • Doesn't the [answer](https://stackoverflow.com/a/21141706/) by the third Dan give the right answer? – Alexey Mar 28 '21 at 12:32

2 Answers2

177

Checkout this thread, it has some useful information about exiting and tracebacks.

If you are more interested in just killing the program, try something like this (this will take the legs out from under the cleanup code as well):

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('Interrupted')
        try:
            sys.exit(130)
        except SystemExit:
            os._exit(130)

[Edited to change the exit code as suggested in comments. 130 is the code typically returned on Linux for a script terminated by Ctrl-C. We may not be on Linux, but the important thing is to return a non-zero value, and 130 is as good as any.]

Daira Hopwood
  • 2,264
  • 22
  • 14
Dan Hogan
  • 2,323
  • 1
  • 16
  • 16
  • 16
    I'd recommend excepting `as`, then grabbing and reusing the exit code. – wizzwizz4 Jul 25 '17 at 15:46
  • 10
    Yes. One should definitly not exit with `0` on KeyboardInterrupt. If anyone is using your script in a script, pipe or what ever they will think your code executed as normal. – user3342816 Sep 22 '19 at 22:07
  • 9
    Linux typically exits with 130: 128 + 2 http://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF , Can not find any cross platform exit-code function on python. But `1` is definitively better then `0` – user3342816 Sep 22 '19 at 22:11
  • I don't disagree with having a better exit code. That said, the code provided was a copy paste from the OP, and typically when you ctrl-c something you don't really care about the exit code as something else has already gone horribly wrong. – Dan Hogan Sep 24 '19 at 12:27
  • 3
    If the user calls the python program from a bash script, and they use `set -e` in the script (as they should), you'd want to interrupt the entire bash script after the user presses CTRL+C. This would require returning a non-zero exit code. – max Jun 13 '20 at 10:16
  • @user3342816 than* – Bersan Oct 22 '20 at 14:23
  • 2
    @Bersan: Thank you! I read up on https://www.grammarly.com/blog/than-then/ , now I only have to remember it :D – user3342816 Dec 20 '20 at 01:30
  • @wizzwizz4 How do I grab the exit code from the exception? – Newbyte Jul 01 '21 at 09:46
  • 1
    @Newbyte `try: ... except SystemExit as e: os._exit(e.code)` or something. – wizzwizz4 Jul 02 '21 at 19:14
  • why try sys.exit() and then fail to os._exit() instead of just going straight to os._exit()? – Josiah Dec 16 '21 at 16:27
  • 1
    @Josiah - This is a super old thread (~8 years). That said there are some specific use cases I had in mind when I wrote this up in 2014, I _think_ it had to do with letting the python logger finish flushing / close any open files etc. Reference this as to the difference between the two calls https://stackoverflow.com/a/9591402/1653168 – Dan Hogan Jan 04 '22 at 20:37
14

You could ignore SIGINTs after shutdown starts by calling signal.signal(signal.SIGINT, signal.SIG_IGN) before you start your cleanup code.

Dan Getz
  • 8,774
  • 6
  • 30
  • 64
  • If the code is expected to be used in other scripts, you should return `signal.SIGINT` to `signal.SIG_DFL` after the cleanup code to return it to its default state. – StickySli Apr 05 '22 at 17:38