Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Some very basic support for the signal module.
Catching signals while another interpreter has registered `signal`
module handlers still does not work, because we don't have a good way to
make the callbacks per-interpreter. We'd need something like PEP 788's
weak reference API to safely keep references to interpreters without
worrying about concurrent deallocation during signal handling.
  • Loading branch information
ZeroIntensity committed Jul 31, 2025
commit ff3bd3f40db5bc4e5656a5a5a841c771c23ac638
11 changes: 6 additions & 5 deletions Doc/library/signal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ This has consequences:
Signals and threads
^^^^^^^^^^^^^^^^^^^

Python signal handlers are always executed in the main Python thread of the main interpreter,
Python signal handlers are always executed in the main Python thread of
intepreters that support signal handling (:c:member:`PyInterpreterConfig.can_handle_signals`),
even if the signal was received in another thread. This means that signals
can't be used as a means of inter-thread communication. You can use
the synchronization primitives from the :mod:`threading` module instead.

Besides, only the main thread of the main interpreter is allowed to set a new signal handler.
Besides, only the main thread is allowed to set a new signal handler.


Module contents
Expand Down Expand Up @@ -421,7 +422,7 @@ The :mod:`signal` module defines the following functions:
same process as the caller. The target thread can be executing any code
(Python or not). However, if the target thread is executing the Python
interpreter, the Python signal handlers will be :ref:`executed by the main
thread of the main interpreter <signals-and-threads>`. Therefore, the only point of sending a
thread of a supporting interpreter <signals-and-threads>`. Therefore, the only point of sending a
signal to a particular Python thread would be to force a running system call
to fail with :exc:`InterruptedError`.

Expand Down Expand Up @@ -523,7 +524,7 @@ The :mod:`signal` module defines the following functions:
any bytes from *fd* before calling poll or select again.

When threads are enabled, this function can only be called
from :ref:`the main thread of the main interpreter <signals-and-threads>`;
from :ref:`the main thread of a supporting interpreter <signals-and-threads>`;
attempting to call it from other threads will cause a :exc:`ValueError`
exception to be raised.

Expand Down Expand Up @@ -578,7 +579,7 @@ The :mod:`signal` module defines the following functions:
above). (See the Unix man page :manpage:`signal(2)` for further information.)

When threads are enabled, this function can only be called
from :ref:`the main thread of the main interpreter <signals-and-threads>`;
from :ref:`the main thread of a supporting interpreter <signals-and-threads>`;
attempting to call it from other threads will cause a :exc:`ValueError`
exception to be raised.

Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2364,6 +2364,8 @@ def test_set___main___attrs(self):
)
self.assertEqual(rc, 0)


class SignalTests(TestBase):
@support.requires_subprocess()
@unittest.skipIf(os.name == 'nt', 'SIGINT not supported on windows')
def test_interpreter_handles_signals(self):
Expand Down Expand Up @@ -2431,6 +2433,25 @@ def test_legacy_interpreter_does_not_handle_signals(self):
self.assertIn(b"KeyboardInterrupt", stderr)
self.assertNotIn(b"AssertionError", stderr)

@unittest.skipIf(os.name == 'nt', 'SIGUSR1 not supported')
def test_signal_module_in_subinterpreters(self):
read, write = self.pipe()
interp = interpreters.create()
interp.exec(f"""if True:
import signal
import os

def sig(signum, stack):
signame = signal.Signals(signum).name
assert signame == "SIGUSR1"
os.write({write}, b'x')

signal.signal(signal.SIGUSR1, sig)
signal.raise_signal(signal.SIGUSR1)
""")
self.assertEqual(os.read(read, 1), b'x')



if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
Expand Down
7 changes: 4 additions & 3 deletions Modules/signalmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ trip_signal(int sig_num)
int fd = wakeup.fd;
if (fd != INVALID_FD) {
PyInterpreterState *interp = _PyInterpreterState_Main();
assert(interp != NULL);
unsigned char byte = (unsigned char)sig_num;
#ifdef MS_WINDOWS
if (wakeup.use_send) {
Expand Down Expand Up @@ -507,7 +508,7 @@ signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
_PyErr_SetString(tstate, PyExc_ValueError,
"signal only works in main thread "
"of the main interpreter");
"of interpreters that support it");
return NULL;
}
if (signalnum < 1 || signalnum >= Py_NSIG) {
Expand Down Expand Up @@ -751,7 +752,7 @@ signal_set_wakeup_fd_impl(PyObject *module, PyObject *fdobj,
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
_PyErr_SetString(tstate, PyExc_ValueError,
"set_wakeup_fd only works in main thread "
"of the main interpreter");
"of supporting interpreters");
return NULL;
}

Expand Down Expand Up @@ -1661,7 +1662,7 @@ signal_module_exec(PyObject *m)
#endif

PyThreadState *tstate = _PyThreadState_GET();
if (_Py_IsMainInterpreter(tstate->interp)) {
if (_Py_ThreadCanHandleSignals(tstate->interp)) {
if (signal_get_set_handlers(state, d) < 0) {
return -1;
}
Expand Down
Loading