How to create a custom KeyboardInterrupt?

I want to create something very similar to the KeyboardInterrupt exception which is invoked after you press Ctrl-C. However, instead of closing the whole program, my exception will be used internally by my code to end the current task. I want it to be invoked via a custom shortcut (I googled it, and you can install custom shortcuts without a problem) and be raised from the currently executing line of code (just like the KeyboardInterrupt does).

How can I do this?

Please note that my code is not async, so an asyncio task cancellation won’t do. Also, I’m ready to use a custom Python build.

Ctrl+C (at least on Unix-like systems) sends a signal to the process. Using a different keyboard shortcut will require a very different approach - for example using a second process or a thread.

I’m fine with using an additional thread. The main problem is to “inject” an exception into a currently running line in the main thread.

You have a few options here. First and foremost, you could simply use Ctrl-C itself, but I assume you’ve looked into that and decided it won’t do.

There are other keystrokes that send signals to a process. On a typical Unix system, you have Ctrl-\ sending SIGQUIT, Ctrl-Z sending SIGTSTP, and sometimes others. Any of these signals - including SIGINT for Ctrl-C - can be caught in Python using the signal module.

Python signal handlers are called when the bytecode interpreter gets around to them, but then they’re executed at the next point in the code. Here’s a (somewhat silly) example:

>>> import signal
>>> def where_am_i(*a): raise Exception("Look! I'm an exception!")
... 
>>> signal.signal(signal.SIGTSTP, where_am_i)
signal.SIG_DFL
>>> def f(): return input("Type something: ")
... 
>>> def g(): return f().upper()
... 
>>> def h(): print(g())
... 
>>> h()
Type something: blah blah blahTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in h
  File "<stdin>", line 1, in g
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in where_am_i
Exception: Look! I'm an exception!

At the prompt, I typed “blah blah blah” and then hit Ctrl-Z, instantly causing the given exception. (If this had been in a script instead of the REPL, the line numbers would have been more interesting.)

If you’re prepared to use either Ctrl-\ or Ctrl-Z as your shortcut, you could use this technique directly (assuming you’re on some sort of Unix - not sure about Windows). For a more custom keyboard signal, you could probably set it up so that, when the keystroke is pressed, the process is sent SIGUSR1; you can then catch SIGUSR1 the exact same way that I catch SIGTSTP above.

In any case, the upshot is that an exception gets raised. At that point, you can define “end the current task” by means of exception handling and stack unwinding, which shouldn’t be too hard (it might be as simple as “try: handle_task() except TaskAbortedException: pass” in some core loop somewhere). At very least, that’s a starting point to go exploring :slight_smile:

Is the user interface graphical, full-screen text (e.g. curses), or command-line (e.g. shell)? Should the keyboard shortcut be global in the desktop environment, or can it limited to just when the application or terminal/console has the keyboard focus? Does the application run on Unix (POSIX) platforms, Windows, or both?

Thank you for your answer.

Using Ctrl-C directly is not an option for me, because I want to preserve the regular KeyboardInterrupt too. Also it would be dangerous to use this feature in this kind of undocumented way, as it might break some third party code today or in the future.

I haven’t tried the other shortcuts yet, but this seems interesting.

Also the example of remapping a signal to an exception is amazing. This seems to be exactly what I need. Do I understand correctly that I can send a signal from the additional thread, but python will call the signal handler in my main thread (because that’s where it was defined), and then inside the handler I can raise an exception which will be injected into the main thread?

I have no GUI, just a console app. The shortcut should only work when the console is focused. The app is for Windows only.

Python signal handlers always execute in the main Thread.

When I catch SIGINT/Ctrl-C I use the signal handler to set a flag. Code
honouring the “interupt” polls the flag regularly. For my code I tend to
exit loops or return from functions early, but you could just as well
raise an exception instead.

Cheers,
Cameron Simpson cs@cskk.id.au

Thanks for clarifying. I will give this a try with my code and post the results here.

There are various ways to implement a keyboard shortcut using the Windows console API. Is this a full-screen console application that’s always reading keyboard/mouse input from the console with low-level events from ReadConsoleInputW()? Or is this a command-line (shell like) application that reads a line of input via high-level ReadConsoleW() (e.g. Python’s input() function or sys.stdin.readline())?

Note that Windows does not implement Unix signals, neither in the kernel nor in the API. Within a process, each C runtime library (there may be multiple) emulates the six ‘signals’ that are required by standard C, including SIGABRT and SIGTERM for use with raise(); SIGFPE, SIGILL, and SIGSEGV based on an OS exception handler; and SIGINT and non-standard SIGBREAK based on console control events.

Console sessions support a small set of control events, including an event for Ctrl+C and an event for Ctrl+Break. Handlers for console control events are always called on a new thread. The CRT registers a console control handler that maps the Ctrl+C and Ctrl+Break events to SIGINT and SIGBREAK. By default this handler just chains to the next control-event handler, which is usually the default handler that calls ExitProcess().

Python supports the C runtime’s emulated ‘signals’ on Windows via the signal module. Initially it registers a handler for SIGINT only, and it leaves SIGBREAK set to the default handler (i.e. SIG_DFL). Python’s C signal handler gets called on the console control-event thread. It simply sets a flag to let the interpreter know that a ‘signal’ was received. The registered handler at the Python level is always called on the main thread. If the main thread is blocked (e.g. waiting to join() another thread, or doing a synchronous I/O read() that blocks such as reading from a pipe), then the ‘signal’ for SIGINT or SIGBREAK won’t be handled until the main thread resumes.