124

Is there some way in Python to capture KeyboardInterrupt event without putting all the code inside a try-except statement?

I want to cleanly exit without trace if user presses Ctrl+C.

0

7 Answers 7

172

Yes, you can install an interrupt handler using the module signal, and wait forever using a threading.Event:

import signal
import sys
import time
import threading

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

signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
forever = threading.Event()
forever.wait()
Sign up to request clarification or add additional context in comments.

7 Comments

Note that there are some platform-specific issues with the signal module -- shouldn't affect this poster, but "On Windows, signal() can only be called with SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, or SIGTERM. A ValueError will be raised in any other case."
Works well with threads, too. I hope you don't ever do while True: continue, though. (In that style, while True: pass would be neater, anyway.) That'd be very wasteful; try something like while True: time.sleep(60 * 60 * 24) (sleeping for a day at a time is an entirely arbitrary figure).
If you're using Chris Morgan's suggestion of using time (as you should), don't forget to import time :)
Calling sys.exit(0) triggers a SystemExit exception for me. You can make it work nicely if you use it in combination with this: stackoverflow.com/a/13723190/353094
You can use signal.pause() instead of sleeping repeatedly
|
49

If all you want is to not show the traceback, make your code like this:

## all your app logic here
def main():
   ## whatever your app does.


if __name__ == "__main__":
   try:
      main()
   except KeyboardInterrupt:
      # do nothing here
      pass

(Yes, I know that this doesn't directly answer the question, but it's not really clear why needing a try/except block is objectionable -- maybe this makes it less annoying to the OP)

4 Comments

For some reason, this doesn't always work for me. signal.signal( signal.SIGINT, lambda s, f : sys.exit(0)) always does.
This doesn't always work with things such as pygtk which use threads. Sometimes ^C will just kill the current thread instead of the entire process, so the exception will only propagate through that thread.
There's another SO question specifically about Ctrl+C with pygtk: stackoverflow.com/questions/16410852/…
@HalCanary Try running the program out of VScode Terminal. Capturing Ctrl-C with try-except doesn't work there in some reason, but it works fine in almost all other environments
33

An alternative to setting your own signal handler is to use a context-manager to catch the exception and ignore it:

>>> class CleanExit(object):
...     def __enter__(self):
...             return self
...     def __exit__(self, exc_type, exc_value, exc_tb):
...             if exc_type is KeyboardInterrupt:
...                     return True
...             return exc_type is None
... 
>>> with CleanExit():
...     input()    #just to test it
... 
>>>

This removes the try-except block while preserving some explicit mention of what is going on.

This also allows you to ignore the interrupt only in some portions of your code without having to set and reset again the signal handlers everytime.

3 Comments

nice, this solution does seem a bit more direct in expressing the purpose rather than dealing with signals.
Using multiprocessing library, I'm not sure on which object I should add those methods .. any clue ?
@Stéphane What do you mean? When dealing with multiprocessing you will have to deal with the signal in both the parent and child processes, since it might be triggered in both. It really depends on what you are doing and how your software will be used.
10

I know this is an old question but I came here first and then discovered the atexit module. I do not know about its cross-platform track record or a full list of caveats yet, but so far it is exactly what I was looking for in trying to handle post-KeyboardInterrupt cleanup on Linux. Just wanted to throw in another way of approaching the problem.

I want to do post-exit clean-up in the context of Fabric operations, so wrapping everything in try/except wasn't an option for me either. I feel like atexit may be a good fit in such a situation, where your code is not at the top level of control flow.

atexit is very capable and readable out of the box, for example:

import atexit

def goodbye():
    print "You are now leaving the Python sector."

atexit.register(goodbye)

You can also use it as a decorator (as of 2.6; this example is from the docs):

import atexit

@atexit.register
def goodbye():
    print "You are now leaving the Python sector."

If you wanted to make it specific to KeyboardInterrupt only, another person's answer to this question is probably better.

But note that the atexit module is only ~70 lines of code and it would not be hard to create a similar version that treats exceptions differently, for example passing the exceptions as arguments to the callback functions. (The limitation of atexit that would warrant a modified version: currently I can't conceive of a way for the exit-callback-functions to know about the exceptions; the atexit handler catches the exception, calls your callback(s), then re-raises that exception. But you could do this differently.)

For more info see:

3 Comments

atexit doesnt' work for KeyboardInterrupt (python 3.7)
Worked for KeyboardInterrupt here (python 3.7, MacOS). Maybe a platform-specific quirk?
Can confirm atexit works for both MacOS and Ubuntu 18.04 for python 3.7 and 3.8
4

You can prevent printing a stack trace for KeyboardInterrupt, without try: ... except KeyboardInterrupt: pass (the most obvious and propably "best" solution, but you already know it and asked for something else) by replacing sys.excepthook. Something like

def custom_excepthook(type, value, traceback):
    if type is KeyboardInterrupt:
        return # do nothing
    else:
        sys.__excepthook__(type, value, traceback)

3 Comments

I want clean exit without trace if user press ctrl-c
This is not true at all. The KeyboardInterrupt exception is created during an interrupt handler. The default handler for SIGINT raises the KeyboardInterrupt so if you didn't want that behavior all you would have to do is provide a different signal handler for SIGINT. Your are correct in that exceptions can only be handled in a try/except however in this case you can keep the exception from ever being raised in the first place.
Yeah, I learned that about three minutes after posting, when kotlinski's answer rolled in ;)
4

I tried the suggested solutions by everyone, but I had to improvise code myself to actually make it work. Following is my improvised code:

import signal
import sys
import time

def signal_handler(signal, frame):
    print('You pressed Ctrl+C!')
    print(signal) # Value is 2 for CTRL + C
    print(frame) # Where your execution of program is at moment - the Line Number
    sys.exit(0)

#Assign Handler Function
signal.signal(signal.SIGINT, signal_handler)

# Simple Time Loop of 5 Seconds
secondsCount = 5
print('Press Ctrl+C in next '+str(secondsCount))
timeLoopRun = True 
while timeLoopRun:  
    time.sleep(1)
    if secondsCount < 1:
        timeLoopRun = False
    print('Closing in '+ str(secondsCount)+ ' seconds')
    secondsCount = secondsCount - 1

Comments

1

If what you want is only to exit without trace but you don’t need to catch the KeyboardInterrupt signal, then you can use sys.excepthook to silent the traceback message and inside it you can use ANSI escape codes to remove the ^C characters.

"\r"      Go to the beginning of line
"\033[2K" Erase line
"\033[A"  Go one line up
import sys


def main():
    input()


def gracefull_KeyboardInterrupt():
    sys.excepthook = lambda type, value, traceback: (
        print("\r" "\033[2K" "\033[A", end="", flush=True)
        if issubclass(type, KeyboardInterrupt)
        else None
    )


if __name__ == "__main__":
    gracefull_KeyboardInterrupt()
    main()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.