EuroPython 2018: Asyncio Today and Tomorrow

Yury Selivanov is a Core CPython developer, author of uvloop, asyncpg, asyncio and multiple PEPs (including async/await: 362, 492, 525, 530, 550, 567).

Brief history

Python 3.3: In 2013, Guido was working on Tulip, which implements PEP 3156. At the same time there were Twisted and tornado, which implemented the async topic in a different was. Tulip became(ish) a part of Python 3.4.

Python 3.4: Provisional asyncio was part of Python 3.4, with low level APIs for protocols, transports, futures, and callbacks. It also included some high level APIs, related to twisted and tornado for many of them.

Python 3.5: Most visibly it included async/await for direct async support, but asyncio was still provisional. It brought new APIs, uvloop. There was also the new Curio framework, which was taken as a learning experience.

Python 3.6: asyncio was now no longer provisional (meaning longer cycles for releases). async generators were added, get_event_loop() was fixed, and new low level APIs introduced. There was also the new Trio framework, which, again, was taken as a learning experience.

Python 3.7: A few months old, it introduces contextvars, native async/await in asyncios source code, asyncio.run() (thanks to Curio), and lots of smaller stuff: sendfile, TLS support, create_task and get_running_loop, BufferedProtocol, and so on.

async/await

It's complex, but the interaface is not all that bad: We have run, gather, create_task, sleep, and the streams API. Under the hood, we have also stuff like loop, but you don't need to care about that.

DON'T

  • Don't @coroutine, will be removed soon
  • Don't use low-level APIs (call_later etc.)

asyncio.run()

asyncio.run() basically wraps getting the event loop, running the async function until it completes, and closing the loop afterwards, only that it does it correctly. Starting with Python 3.7, please use asyncio.run(), which provides all the correctness and convenience that you don't want to build yourself. You won't need the loop, just an entry point you pass to run, and then you should use async/await everywhere. Don't pass the loop anywhere!

It's used in serve_forever(), for example, which you should use, too!

contextvars

Use contextvars (see PEP 550, PEP 567, and about 900 emails on python-ideas).

It's magic, and comes with Python 3.7 in the standard library (which also uses it in its code in decimal).

import contextvars


task_id = contextvars.ContextVar('task trackng ID')
task_id.set('unique number')

Use contextvars for monitoring (e.g. running duration), localization (e.g. current language for HTTP request lifecycle), security for permissions, debugging (ALL THE VARIABLES), or execution contexts (such as decimal contexts or numpy error contexts).

What's next in Python 3.8?

Let's look at Trio – it was designed from scratch and is incompatible with asyncio. It's focussed primarily with usability and gets a lot of stuff right. Look at this intro!. Trio's wording includes children, parents, nurseries, etc – very vivid images. Nurseries have almost no out of order execution, they have a traceable control flow, don't use exceptions, etc – they're great. What can we learn from Trio?

  • We need better and proper exception handling – which Yury already has ideas for for Python 3.8, in the form of create_supervisor. This would return an asynchronous context manager, which can be passed around. (Which would still be used as a low level interface.). Any unhandled exception would propagate correctly and be able to clean up after itself.
  • Another thing we could learn from Curio are TaskGroup objects with a better API than asyncio.gather(), and it makes it easy to schedule tasks in buckets.
  • Maybe we'll also get a low level event loop tracing API.
  • Documentation will also still be worked on.
  • We may get timeout and cancel scopes like in Trio (see here).
  • We may get a new streams API, because the current API is inconsistent and hard to use.
  • We may get a context manager for shield() (shielding coroutines from cancellation).
  • We will probably get SSL over SSL.
  • And we will probably make CancelledError a BaseException, inheritance wise. Since this is backwards incompatible though, it might take a while.

And please be aware that the Python Core devs hear your concerns and pain points – keep in touch, voice your thoughts, and be hopeful for further improvements.