Unravelling `async` and `await`

For this post in my Python syntactic sugar series, I am going to cover async and await.

Now when I started to think about this post I was worried it was going to be rather long and arduous to research (although I believe class is going to ultimately win that crown 😜), but then I remembered I had written a blog post about how async and await worked in Python 3.5. As part of that post I went through the history of how Python got to that point of asynchronous programming, which meant I had already dived into the details of how Python evolved earlier syntax to what it has today! Thus this post is going to draw heavily from my async history post to save myself some time, so if you feel I skimmed over details then chances are it's because I covered it in my other post.

Unravelling async

Let's go from the outside in, tackling async first. It turns out that unravelling async def is very straightforward thanks to the types.coroutine decorator which is what async def evolved from. This decorator sets a flag on the code object for the generator to distinguish it from any other plain generator. Otherwise that's it as async functions are fundamentally generators.

Unravelling await

The await expression evolved from yield from, but with two key tweaks: checking that the object is awaitable, and also supporting awaitable objects which define __await__().

The standard library provides the inspect.isawaitable() function to determine whether an object is awaitable. One tweak down.

The other tweak is how to call the coroutine via yield from. Awaitables can either define __await__() which returns an iterable or be a generator marked as a coroutine. As such, we need to support both situations. As with previous instances of special methods, we need to get the __await__() method from the type directly and then call it with the object. Otherwise both types of awaitables end up with an object that can be passed to yield from.

def _await(coroutine):
    """Simulate `await coroutine`."""
    if not inspect.isawaitable(coroutine):
        msg = f"object {builtins.type(coroutine)} can't be used in 'await' expression"
        raise TypeError(msg)
    coroutine_type = builtins.type(coroutine)
    try:
        __await__ = _mro_getattr(coroutine_type, "__await__")
    except AttributeError:
        awaitable = coroutine
    else:
        awaitable = __await__(coroutine)
    yield from awaitable
Code to simulate await

Thanks to how Python built up to async and await we already had the building blocks to unravel the syntax and essentially devolve to earlier versions of Python!