KeyboardInterrupt instance have an exit code

If ^C is a normal way to exiting your program, you probably don’t want the traceback printed. So you do something like this.

try:
    main()
except KeyboardInterrupt:
    raise SystemExit(1)

If a KeyboardInterrupt is not handled, the interrupter exits with a special exit code. If I wanted to preserve the exit code, I do something like this:

_prev_excepthook = sys.excepthook

def _excepthook(type, value, traceback):
    if not isinstance(value, KeyboardInterrupt):
        _prev_excepthook(type, value, traceback)

sys.excepthook = _excepthook

But that’s a lot of code and affects global state. If KeyboardInterrupt had an exit code, we could so something like this:

try:
    main()
except KeyboardInterrupt as error:
   raise SystemExit(error.exit_code)
2 Likes

Exit codes of keyboard interrupts are weird. If I put raise KeyboardInterrupt in a Python script and run it from a terminal, it uses exit code 130 but if I run it as a subprocess, it’s -2:

> python test.py
[SIGINT]> echo $status
130
> python
>>> import sys, subprocess
>>> subprocess.run([sys.executable, "test.py"])
CompletedProcess(args=['/home/brenainn/.pyenv/versions/3.12.11/bin/python3.12', 'test.py'], returncode=130)
>>> subprocess.run([sys.executable, "test.py"])
CompletedProcess(args=['/home/brenainn/.pyenv/versions/3.12.11/bin/python3.12', 'test.py'], returncode=-2)

I don’t even know how to intentionally return -2 – the obvious sys.exit(-2) actually gives exit code 254.

So +1 for a more obvious way to preserve whatever’s going on in there.

2 Likes

import os; os.kill(os.getpid(), 2)

130 is not the exit code of the program.

Codes > 128 are the signal a program was terminated with.
128 + 2 (keyboard interrupt) = 130

2 Likes

I see. So does Python or the OS decide the exit code is 130?

The OS in the case of Linux (and MAC OS X). I don’t do Windows. For example, this C program returns 2 if allowed to run for 8 seconds, 130 if Ctrl-C is pressed, 143 if killed (with default 15/SIGTERM), 137 if killed with 9/SIGKILL from another terminal.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
	printf("%d\n",getpid());
	sleep(8);
	exit(2);
}
1 Like

Python must have something to do with because if run this script:

raise KeyboardInterrupt()

My terminal reports an exit code of 130. i.e.;

$ python example.py
$ echo $?
130

But a signal was never involved. Am I missing something?

1 Like

The interpreter will exit by sending itself a SIGINT (after defaulting the signal handler) if there is a KeyboardInterrupt that goes unhandled.

1 Like

I see. So it’s that behavior I want to preserve without Python printing the trace back for the unhandled KeyboardInterrupt. So maybe what I really want is something like this:

try:
    main()
except KeyboardInterrupt as error:
    error.quiet = True
    raise error

Then you could make a nice context manager:

@contextmanager
def quiet_unhandled_keyboard_interrupt():
    try:
        yield
    except KeyboardInterrupt as error:
        error.quiet = True
        raise error

with quiet_unhandled_keyboard_interrupt():
    main()

(Probably needs a catchier name :))

True on the *nix systems. Windows is a little different. 3.14 source

static int
exit_sigint(void)
{
    /* bpo-1054041: We need to exit via the
     * SIG_DFL handler for SIGINT if KeyboardInterrupt went unhandled.
     * If we don't, a calling process such as a shell may not know
     * about the user's ^C.  https://www.cons.org/cracauer/sigint.html */
#if defined(HAVE_GETPID) && defined(HAVE_KILL) && !defined(MS_WINDOWS)
    if (PyOS_setsig(SIGINT, SIG_DFL) == SIG_ERR) {
        perror("signal");  /* Impossible in normal environments. */
    } else {
        kill(getpid(), SIGINT);
    }
    /* If setting SIG_DFL failed, or kill failed to terminate us,
     * there isn't much else we can do aside from an error code. */
#endif  /* HAVE_GETPID && !MS_WINDOWS */
#ifdef MS_WINDOWS
    /* cmd.exe detects this, prints ^C, and offers to terminate. */
    /* https://msdn.microsoft.com/en-us/library/cc704588.aspx */
    return STATUS_CONTROL_C_EXIT;
#else
    return SIGINT + 128;
#endif  /* !MS_WINDOWS */
}
1 Like

This just forwards the signal to the OS:

import signal
import time


class QuitCtrlC:

    def __enter__(self):
        self.previous_signal = signal.signal(signal.SIGINT, signal.SIG_DFL)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        signal.signal(signal.SIGINT, self.previous_signal)


with QuitCtrlC():
    s = 5
    print(f"Sleeping {s} seconds, quiet")
    time.sleep(s)
print(f"Sleeping {s} seconds, default Python")
time.sleep(s)

python3 kb.py
Sleeping 5 seconds, quiet
^C
beth:tmp gerardweatherby$ echo $?
130

beth:tmp gerardweatherby$ python3 kb.py
Sleeping 5 seconds, quiet
Sleeping 5 seconds, default Python
^CTraceback (most recent call last):
File “/private/tmp/kb.py”, line 18, in
time.sleep(s)
~~~~~~~~~~^^^
KeyboardInterrupt

beth:tmp gerardweatherby$ echo $?
130

1 Like

This pretty cool. Almost everything I do is on Linux, so this would probably be enough for me. I still think there’s room for lower-level support so you can easily get the exact same behavior as letting it go unhandled.

Actually, this doesn’t quite work. For example, atexit callbacks don’t get called.

atexit callbacks called:

import atexit, sys, time
atexit.register(lambda: print('Goodbye'))
_prev_excepthook = sys.excepthook

def excepthook(exc_type, exc_value, traceback):
    if not isinstance(exc_value, KeyboardInterrupt):
        _prev_excepthook(exc_type, exc_value, traceback)

sys.excepthook = excepthook
time.sleep(5) # ^C here

atexit callbacks not called:

import atexit, signal, sys, time
atexit.register(lambda: print('Goodbye'))
signal.signal(signal.SIGINT, signal.SIG_DFL)
time.sleep(5) # ^C here