[asyncio] Skipping signal handling setup during import for Python embedded context

Python supports initializing the interpreter with signal handling disabled: Initialization, Finalization, and Threads — Python 3.12.0 documentation

As the documentation states, this might be useful when Python is embedded.

Personally, I think its entirely reasonable that, within this embedded context, an application may choose to import and run asyncio. However, this has the unexpected side-effect of setting up asyncio signal handlers, which I’m sure there are lots of great reasons for.

This means that the parent application that has embedded Python must wait until after this import to then setup signal handlers, which is a mild inconvenience.

I’m not an expert as to all the intricacies of signal handling in asyncio, but would it be reasonable to ask that asyncio provide some way to skip this signal handling process? Since its happening during import, I don’t know how I would disable the setup process.

1 Like

Hm. Not much happens in asyncio related to signals except import signal – setting signal handlers only happens when an event loop is created (e.g. by asyncio.run()).

Perhaps the initialization you’d like to skip happens implicitly as a result of importing the signal module? I’m not sure how to help you, as I’m no expert myself on Py_InitializeEx() behavior.

Yeah, it looks like I can reproduce this behavior by importing signal. Should I create a separate forum post to discuss that?

Regarding asyncio’s signal handling setup in run, if I happen to be on the main thread but still want to skip signal handling setup, I can call signal.signal(<signal>, <not_default_int_handler>) beforehand.

But that seems prone to failure if the logic changes in the future (though I don’t know how likely that is). Would it be worth adding some explicit option that allows skipping signal handling setup?

Can you clarify what exactly it is that import signal does that you would like to suppress?

When signal is imported, I want to tell it not to override the default signal handler (SIG_DFL).

There are a couple of ways around this, and neither of them is objectively bad. But it would be nice if there was an even better alternative.

Consider the following snippet:

(0) Py_InitializeEx(0);
(1)
(2) char* exp =
(3)   "input('press ctrl-c or enter’);"
(4)   "import signal;"
(5)   "signal.signal(signal.SIGINT, signal.SIG_DFL);" // this is one workaround
(6)   "input('press ctrl-c');";
(7) PyRun_SimpleString(exp);
(8)
(9) Py_FinalizeEx();

Line (0): I have initialized embedded Python, skipping signal handler registration.
Line (3): Press ctrl-c to show the program exits without also raising KeyboardInterrupt, otherwise press enter to continue execution.
Line (5): I must manually reset the signal handler to SIG_DFL if I don’t want to see KeyboardInterrupt raised.
Line (6): Press ctrl-c to show the program exit without also raising KeyboardInterrupt.

After removing line (5), line (6) will then raise KeyboardInterrupt.

Alternatively, I can insert a new line at (0):

signal(SIGINT, <my signal handler>); // this is another workaround

Now, the signal library will respect my custom handler.

Of course, if I’m intentionally initializing with Py_InitializeEx(0), I shouldn’t need to import signal. And I don’t, but I may want to import asyncio, which imports signal.


Ok, now why would I would I care about this particular scenario? I admit I don’t have a mainstream use case.

But I’m working on a Rust wrapper around a Python library. I use another library to help me with inter-op between Rust and Python async.

Part of what this library does (before running any of the Rust code) is it initializes a Python interpreter via Py_InitializeEx(0) and also imports asyncio.

Any Rust developers using this library would then learn that, unless they register any signal handlers, SIGINT will end up raising KeyboardInterrupt.

Solvable, but it’s unexpected behavior (or at least it was for me) until one learns what is happening underneath the hood.

If this is something that won’t be solved at the CPython level (due to infeasibility or if the costs outweigh the benefits), then I plan on moving forward with one of the workarounds.

It will definitely not be solved before 3.13 comes out (in about a year), and won’t be solved for older versions.

If you want to solve this for 3.13, I recommend that you try to push for a change (probably in our GitHub issue tracker). The tricky bit is that currently that flag for Py_InitializeEx() ends up deciding whether _signal is imported. If we wanted to decouple that we’d have to do a bunch of tricky stuff – for compatibility we’d probably have to introduce a new config variable to tell the _signal module not to override the `SIGINT1 handler.

One other thought: you can set a SIGINT handler from C code before calling Py_InitializeEx(0). As long as it isn’t set to SIG_DFL, the _signal module initialization won’t change it.

Looks like setting a non-SIG_DFL handler for SIGINT will be the way to go for now.

Idealistically, I think it makes sense for the flag passed to Py_InitializeEx to be propagated down to the _signal module, so I’ll try to push for a change via the GitHub issue tracker.

Thanks for helping me think through this.