Introducing QtAsyncio in technical preview
February 19, 2024 by Adrian Herrmann | Comments
Qt for Python goes beyond just Qt and beyond just Python: It’s the best of both worlds! 🤝 But Python is more than only a language 🐍, and Python users expect wide interoperability between their favorite libraries and modules of the huge Python ecosystem. If you want to write an application with asynchronous I/O, asyncio
is a popular choice that allows you to use the async
and await
syntax, and to work with coroutines (if you don't know what those are, imagine them as "asynchronous functions"). It is part of the standard library, the foundation of several async I/O frameworks such as AIOHTTP and FastAPI, and it's even used to write Telegram & Discord bots. (It's true! ☝️) In general, an async framework is a good choice when your program needs to handle many I/O operations from many sources, like a web server.
As you surely know, Qt is based on an event loop, and conveniently, so is asyncio. So what is the obvious choice if you are an engineer or maybe just insane? 🤔 Throw both into a blender and see what happens, of course! 🧪
asyncio offers an extensive API to implement custom event loops that can then be used instead of the default implementation. During the past months, we have been working exactly on that: QtAsyncio, an implementation of asyncio's API based on Qt, through which applications can use asyncio together with Qt . This positions Qt for Python as an excellent choice for asynchronous program logic in combination with one of the established libraries of the Python ecosystem, enabling developers to leverage their respective strengths. Now, with the release of Qt for Python 6.6.2, we are happy to announce QtAsyncio in technical preview.
Get started with QtAsyncio
To write a program with QtAsyncio, first import the module, e.g.:
import PySide6.QtAsyncio as QtAsyncio
QtAsyncio provides a function run()
that can be used to run a specific coroutine until it is complete, or to start the Qt & asyncio event loop plainly. Additional optional arguments configure whether the event loop should stop after the coroutine's completion, and whether the QCoreApplication at the core of QtAsyncio should be shut down when asyncio finishes:
QtAsyncio.run(coro=None, keep_running=True, quit_qapp=True)
Check out the documentation for more details on how to use this function.
Example
The following is a very simple example that shows how you can make Qt and asyncio interoperate through QtAsyncio, creating and managing an asyncio-based task through user interaction in a Qt-based UI.
We start by creating a simple QApplication
and a window object of a derived class. This MainWindow
will contain our UI and a coroutine:
app = QApplication(sys.argv)
main_window = MainWindow()
MainWindow
contains a text field.
self.text = QLabel("The answer is 42.")
This text field will be edited by our coroutine. This is triggered by a QPushButton
. Pushing this button will schedule the coroutine set_text
in QtAsyncio's event loop:
async_trigger = QPushButton(text="What is the question?")
async_trigger.clicked.connect(lambda: asyncio.ensure_future(self.set_text()))
And this is the code inside the coroutine. Since we're now in a coroutine and not a function (as we would be with a normal Qt slot), we are not limited to synchronous code, and we can also execute asynchronous code by awaiting other coroutines.
async def set_text(self):
await asyncio.sleep(1)
self.text.setText("What do you get if you multiply six by nine?")
This is what it looks like in action:
You can check out the example's entire code here.
Technical preview
The asyncio API can be divided in two levels: 🪜
- Fundamental infrastructure for event loops and asynchronous operations, including futures, tasks, handles executors, and event loop management functions.
- A user-facing API for use in applications, including transports protocols, network connections, servers, sockets, signals, and subprocesses.
QtAsyncio currently covers the first level in its entirety, including functions like run_until_complete(), run_forever(), stop(), close(), call_soon(), create_future(), create_task(), and more. For all these functions, QtAsyncio's API is identical to asyncio's (which is the idea ☝️). Also included is the ability to run synchronous code in an executor. We have begun work on implementing the second API level, so expect further additions in future Qt for Python releases.
Your feedback is valuable to us! You can help us determine which parts of the API you consider the most important. Do you want to write a web server with a fancy Qt-powered UI? Do you dream of IPC at night? Is subprocess support just the right thing your application needs to achieve transcendence? 😇 Your comments can help us prioritize. 🏎️ Also feel free to create a JIRA issue or feature request - be sure to select QtAsyncio as the component.
Coroutines explained
Coroutines are functions that can be paused (yield) and resumed. Behind this simple concept lies a complex mechanism that is abstracted by the asynchronous framework. This talk presents a diagram that attempts to illustrate the flow of a coroutine from the moment it's provided to the async framework until it's completed. Check it out if you'd like to learn more about coroutines and QtAsyncio!
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.9 Now!
Download the latest release here: www.qt.io/download.
Qt 6.9 is now available, with new features and improvements for application developers and device creators.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.
Commenting for this post has ended.
Now do C++ coroutines!
See https://qcoro.dvratil.cz/ and how I use it in ScreenPlay, to export a Godot project to display it as a desktop wallpaper:
Return a generic Result Gadget back to qml: https://gitlab.com/kelteseth/ScreenPlay/-/blob/master/ScreenPlayUtil/inc/public/ScreenPlayUtil/util.h?ref_type=heads#L65
Called from QML: https://gitlab.com/kelteseth/ScreenPlay/-/blob/master/ScreenPlay/qml/Installed/Sidebar.qml?ref_type=heads#L380
Do the heavy lifting in C++: https://gitlab.com/kelteseth/ScreenPlay/-/blob/master/ScreenPlayUtil/src/util.cpp?ref_type=heads#L634
It would be nice to have it in Qt directly. It's a C++ framework after all. Also QCoro has one fatal flaw - it doesn't (yet) support automatic cancellation of coroutines based on QObject lifetime (like how signal connections are cancelled when owners of signal or slot ar destroyed).
Yes, see my bug report here https://bugreports.qt.io/browse/QTBUG-101025
I am fairly new to Qt, primarily through a project in which I am having to use Python and where I decided to use Qt for Python / PySide6 for the GUI. However, one thing that confuses me is why Qt likes to be everything, replacing many parts of the host language, and not just a framework. This seems to be yet another continuance of that.
Why doesn't Qt just integrate with the default
asyncio
event loop? Why is Qt recreating theasyncio
API and notably only a subset of that API? Python's documentation and examples are more extensive, whereas the Qt for Python docs are incomplete and very sparse. What does a user do when anasyncio
-based library has issues with Qt's implementation of the event loop? Why do I need to use Qt for all the protocols instead of justasyncio
's streams and the third-partywebsockets
library, for example?For what it's worth, my entire application is built using
asyncio
, but I keep it in its own thread and Qt in its own thread. The Qt application sends theasyncio
event loop messages via anasyncio.Queue
andasyncio.run_coroutine_threadsafe
. (Note that that function isn't even provided by Qt's async API.)So integrating Qt into an
asyncio
application is already doable. While I was initially intrigued by this announcement, using it would require me to rewrite all of myasyncio
-based code to use Qt's implementation of that interface.I strongly agree with what has been said here, and specifically with:What does a user do when an asyncio-based library has issues with Qt's implementation of the event loop?
I'm currently using an asyncio implementation for gRPC in python but when I tried to use
QtAsyncio
for my application, things started to break in this library as theQAsyncioEventLoop
doesn't implement some of theAbstractEventLoop
methods....To be more specific,
QAsyncioEventLoop
inheritsBaseEventLoop
which has implementation for some of the needed methods but for some reason theQAsyncioEventLoop
just reimplements them to raise aNotImplementedError
.This seems very wrong to me, if there is any reason for "not implementing" these already implemented methods, at least add a note or some comments so we can understand why and how to overcome this...
When import, I got this: AttributeError: module 'asyncio' has no attribute 'AbstractChildWatcher'.
Is there any way to do clean up before the event loop ends but after the Qt application has ended?
Does this support QML applications? In particular, I am using the
@QmlElement
and@QmlSingleton
decorators on a Python class to interface with the QML part of the application. Does thisasyncio
support using a Qt Singleton such that the methods on the class can beasync
?NotImplementedError: QAsyncioEventLoop.create_connection() is not implemented yet
is there any plan to implement it?
Waiting on this too... sadly I found no updates on this apart from trying to follow these links
https://bugreports.qt.io/browse/PYSIDE-769
https://codereview.qt-project.org/q/QtAsyncio