It's clear that Python's signal module doesn't provide the necessary tool for accessing raw C function pointers, which are required to save and restore signal handlers installed outside Python. But there's an escape hatch: as a solution of last resort, we can invoke raw libc function signal() directly from Python via ctypes.
Discussion
In the C programming language, what we want is essentially:
/* declare a pointer to the original signal handler function */
void (*sighandler_original)(int);
/*
* In pure C (without assuming POSIX), it's not possible to get the
* original signal handler without changing it. So we first change
* SIGINT's handler to SIG_IGN to get the original handler's pointer,
* then we change it back.
*/
sighandler_original = signal(SIGINT, SIG_IGN);
signal(SIGINT, sighandler_original);
The equivalent Python code, using ctypes, would be:
import ctypes
import ctypes.util
# On GNU/Linux the result is "libc.so.6"
libc_filename = ctypes.util.find_library("c")
libc = ctypes.cdll.LoadLibrary(libc_filename)
# declare libc signal()'s arguments and return types
# incorrect types declarations may create unpredictable results
libc.signal.argtypes = [ctypes.c_int, ctypes.c_void_p]
libc.signal.restype = ctypes.c_void_p
# C macro SIGINT 2
# C macro SIG_IGN 1
sighandler_original = libc.signal(2, 1)
libc.signal(2, sighandler_original)
After running this code snippet, sighandler_original is now an integer that contains the memory address of the original C signal handler.
>>> print(sighandler_original)
140043040324672
To restore the handler in the future, simply execute libc.signal(2, sighandler_original) again.
You can see that an obvious problem is that ctypes only knows about calling a function via the system's Application Binary Interface (ABI), but it doesn't know anything about C headers or macros. Python also has no idea about the proper input and output types of functions, of which we must explicitly tell Python. Incorrect declarations may produce unpredictable results. This can potential be a portability pitfall when using ctypes for other applications.
Here we makes some specific assumptions of the values of SIGINT and SIG_IGN, which are 2 and 1. They're extremely unlikely to change on Unix systems. We also assume void * and a function pointer are the same thing, which is valid for POSIX.
Full Solution
To make the code more readable in this particular case, we can use the constants already defined in the high-level signal module, so the improved solution is:
import signal
import ctypes
import ctypes.util
# On GNU/Linux the result is "libc.so.6"
libc_filename = ctypes.util.find_library("c")
libc = ctypes.cdll.LoadLibrary(libc_filename)
# declare libc signal()'s arguments and return types
# incorrect type declarations may create unpredictable results
libc.signal.argtypes = [ctypes.c_int, ctypes.c_void_p]
libc.signal.restype = ctypes.c_void_p
sighandler_original = libc.signal(signal.SIGINT, signal.SIG_IGN)
libc.signal(signal.SIGINT, sighandler_original)
# change signal handler via Python
# (assuming handle_SIGINT is already defined in Python)
signal.signal(signal.SIGINT, handle_SIGINT)
# ...
# do things in Python
# ...
# restore signal handler via libc on exit
libc.signal(signal.SIGINT, sighandler_original)
Note that signal.SIGINT and signal.SIG_IGN are custom types, not raw integers or pointers. But since we have libc.signal.argtypes, ctypes automatically type-cast them for us.
Comments
Due to the risks involved, the low-level libc.signal() should strictly be used at the beginning and the end of the Python program for saving and restoring signal handlers. It should not be used for other purposes, and especially do not use it as a replacement of Python's high-level signal.signal().
Possible improvements, such as replacing the deprecated ANSI C signal() with POSIX sigaction() is left as an exercise for the reader.
It's not popularly known, but Python's standard library actually exposes a considerable amount of low-level system details. For example, the os module exposes fork(), exec(), and pipe(), thus it's totally possible to write an asynchronous program in the traditional Unix way instead of using Python's own threading or multiprocessing (historically, Perl programmers were more open to this idea, not Python). Similarly, it's possible to call an arbitrary C function via the module ctypes, which is largely the equivalent of C's dlopen() , allowing access to arbitrary shared libraries. Of course, their use is actively discouraged by Python developers, since the result is a footgun without any safety guarantee from Python, and depends on the specifics of a particular system.
But if you know what you're doing, it's always a choice.