The question user2357112 linked, explains this in some way: Why can't I handle a KeyboardInterrupt in python?.
The keyboard interrupt is raised asynchronously, so it does not immediately terminate the application. Instead, the Ctrl+C is handled in some kind of event loop that takes a while to get there. This unfortunately means that you cannot reliably catch the KeyboardInterrupt
in this case. But we can do some things to get there.
As I explained yesterday, the exception that stops the raw_input
call is not the KeyboardInterrupt
but an EOFError
. You can easily verify this by changing your bing
function like this:
def bing():
try:
raw_input()
except Exception as e:
print(type(e))
You will see that the exception type that’s printed is EOFError
and not KeyboardInterrupt
. You will also see that the print
did not even go through completely: There is no new line. That’s apparently because the output got interrupted by the interrupt which arrived just after the print statement wrote the exception type to stdout. You can see this also when you add a bit more stuff to the print:
def bing():
try:
raw_input()
except EOFError as e:
print 'Exception raised:', 'EOF Error'
Note that I’m using two separate arguments here for the print statement. When we execute this, we can see the “Exception raised” text, but the “EOF Error” won’t appear. Instead, the except
from the outer call will trigger and the keyboard interrupt is caught.
Things get a bit more out of control in Python 3 though. Take this code:
def bing():
try:
input()
except Exception as e:
print('Exception raised:', type(e))
try:
bing()
print('After bing')
except KeyboardInterrupt:
print('Final KeyboardInterrupt')
This is pretty much exactly what we did before, just amended for Python 3 syntax. If I run this, I get the following output:
Exception raised: <class 'EOFError'>
After bing
Final KeyboardInterrupt
So we can again see, that the EOFError is correctly caught, but for some reason Python 3 continues the execution a lot longer than Python 2 here, as the print after bing()
is executed as well. What’s worse, in some executions with cmd.exe, I get the result that no keyboard interrupt is caught at all (so apparently, the interrupt got processed after the program already completed).
So what can we do about this if we want to make sure that we get a keyboard interrupt? One thing we know for sure is that interrupting an input()
(or raw_input()
) prompt always raises an EOFError
: That’s the one consistent thing that we have seen all the time. So what we can do is just catch that, and then make sure that we get the keyboard interrupt.
One way to do this would be to just raise a KeyboardInterrupt
from the exception handler for EOFError
. But this not only feels a bit dirty, it also doesn’t guarantee that an interrupt is actually what terminated the input prompt in the first place (who knows what else can possibly raise an EOFError?). So we should have the already existing interrupt signal generate the exception.
The way we do this is quite simple: We wait. So far, our problem was, that the execution continued because the exception didn’t arrive fast enough. So what if we wait a bit to let the exception eventually arrive before we continue with other things?
import time
def bing():
try:
input() # or raw_input() for Python 2
except EOFError:
time.sleep(1)
try:
bing()
print('After bing')
except KeyboardInterrupt:
print('Final KeyboardInterrupt')
Now, we just catch the EOFError and wait a bit to let the asynchronous processes in the back settle and decide on whether to break the execution or not. This consistently allows me to catch the KeyboardInterrupt
in the outer try/catch and will not print anything else except what I do in the exception handler.
You might worry that one second is a long time to wait, but in our cases, where we interrupt the execution, that second never really lasts long. Just a few milliseconds after the time.sleep
, the interrupt is caught and we’re in our exception handler. So the one second is just a fail-safe that will wait long enough that the exception definitely arrives in time. And in the worst case, when there isn’t actually an interrupt but just a “normal” EOFError? Then the program that was previously blocking infinitely for user input will take a second longer to continue; that shouldn’t really be a problem ever (not to mention that the EOFError is probably super rare).
So there we have our solution: Just catch the EOFError, and wait a bit. At least I hope that this is a solution that works on other machines than my own ^_^" After last night, I’m not too sure about this—but at least I got a consistent experience over all my terminals and different Python versions.