Introducing QtAsyncio in technical preview

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:

qtasyncio_minimal_demo

You can check out the example's entire code here.

Technical preview

The asyncio API can be divided in two levels: 🪜

  1. Fundamental infrastructure for event loops and asynchronous operations, including futures, tasks, handles executors, and event loop management functions.
  2. 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