Removing the asyncio policy system (asyncio.set_event_loop_policy) in Python 3.15

for glibcoro the preferred code would be:

asyncio.run(main(), loop_factory=GLibEventLoop)

for Qt For Python the preferred code would be:

def loop_factory():
    application = QCoreApplication.instance()
    if application is None:
        application = QCoreApplication()
    return QAsyncioEventLoop(application)

async def main():
    await eratosthenes.start()
    await asyncio.Event().wait()

asyncio.run(main(), loop_factory=loop_factory)

That code presumably belongs in every single user’s app though, whereas right now (I assume) the library simply handles it transparently.

Do we have a migration path where frameworks can set the default loop factory? Rather than pushing the required change onto every single one of their users?

1 Like

uvloop provides a uvloop.run the recommendation could be to follow that pattern so you get a glibcoro.run(main()) and a PySide6.QtAsyncio.run(main())

2 Likes

Again, that is still forcing the migration effort onto users of the framework.

Do we have a migration path where the framework can set the default loop factory?

2 Likes

You could overwrite asyncio.EventLoop but the scary thing there is that it affects all threads. What if I wanted a background thread to continut to just run the original default EventLoop?

Are those frameworks auto-installing their own policy as a side effect of being imported? Does the policy they install create an event loop of their type for all threads? If so, overwriting EventLoop is not much of a regression. If they are careful to only do something special for the main thread (or for the first event loop created or whatever) we may have to provide a mechanism to set a loop factory per thread, for example. At that point might we not just keep the existing policy system for that purpose, rather than retiring that and inventing something new to replace it?

2 Likes

(If those questions were meant for me: I don’t know. Let me know if you’d like me to ask, but I only use asyncio & GUI tools as a user, so I’d make a poor middleman.)

It would be helpful if someone could contact maintainers from those projects (specifically Qt, glibcoro, possibly Jupyter) and have their feedback on this thread or in the GitHub issue.

@encukou Could you help with this?

Consider Jupyter pinged, I’ll take a look tomorrow.

We set the event loop policy on Windows in Jupyter apps to comply with Tornado, which only supported the selector event loop, until Tornado 6.1. We can now use the proactor loop, but it is slower since tornado has to use select in different thread. Running our jupyter_server test suite with the selector event loop takes ~10 min and with proactor ~15 min.

A test isn’t a benchmark though. Nevertheless, could you link to the place in your code where you set the event loop policy?

Anyway, the recommended user action if we go ahead with deprecating and eventually scrapping the policy system would be to explicitly pass an event loop factory (e.g. asyncio.SelectorEventLoop) to whatever code starts the application. If you’re using asyncio.run(), you can pass it easily to that function.

How onerous would that be for you to impose on your user community?

Sure. I’ve pinged glibcoro and PySide.

Here is where we set the policy.

As it turns out, we were still using the legacy method of starting a tornado io_loop.start() explicitly in jupyter_server and ipykernel, which are in turn used by notebook and jupyterlab. I believe that in all of our use cases, we can can actually use asyncio.run() instead, but it will take some experimentation to verify.

Interesting. Regardless of the issue of whether you can use asyncio.run() – the docstring gripes about asyncio missing *_reader methods on the proactor loop. Did anyone (either from Jupyter or from Tornado) ever file an issue about that?

Yes, @bdarnell raised the issue: Configuration of windows event loop for libraries · Issue #81554 · python/cpython · GitHub

Hello, I’m one of the maintainers of Qt for Python and actively working on its asyncio support. Our examples use asyncio.get_event_loop().run_forever() instead of asyncio.run(coro), as the latter approach does not work when we want to keep the event loop running even after the given coroutine finished, which would be the case for a graphical program that waits for user input (the Qt event loop keeps waiting for GUI events). Therefore, the suggested new method of passing along a loop_factory argument to asyncio.run() wouldn’t work for us. What would be your advice for such a case?

I’m also a developer of a UI based Python application which can be hosted within various native UI frameworks such as Qt. The basic architecture is the UI framework makes calls into our Python library which can queue async code, and then the UI framework also periodically calls a function periodic in which we execute pending async code. We use asyncio.get_event_loop() to create the event loop. Then in periodic we do event_loop.stop() and event_loop.run_forever() to execute any outstanding async code but not new async code created during this particular run_forever call. On Windows we set the policy to use WindowsSelectorEventLoopPolicy before get_event_loop.

@adrianghc:

A little hackish, but you could just sleep for a really long time:

asyncio.run(asyncio.sleep(1e9), loop_factory=...)

That’s over 30 years.:slight_smile:

Or you could use an event that never completes:

asyncio.run(asyncio.Event().wait(), loop_factory=...)

Or you could just let the user instantiate your custom loop class (instead of having them set the event loop policy), and then have them call loop.run_forever().

It would probably make sense to offer a helper function to do this, which you could provide for older Python versions as well. (Though I commend you for trying to use asyncio’s lower-level mechanisms directly – this feels useful for users who want to customize the initialization order.)

@cmeyer

The loop.stop() / loop.run_forever() trick should continue to work, it’s unrelated to policies. The iffy thing is using get_event_loop() to create an event loop – this was poor API design and we’re cracking down on code that relies on this behavior – though we no longer plan to remove that function altogether (its behavior would just be fixed rather than going through the policy).

You could just write a few lines of code that passes asyncio.SelectorEventLoop as a loop factory.

1 Like