Home > Articles

This chapter is from the book

This chapter is from the book

5.23 Asynchronous Functions and await

Python provides a number of language features related to the asynchronous execution of code. These include so-called async functions (or coroutines) and awaitables. They are mostly used by programs involving concurrency and the asyncio module. However, other libraries may also build upon these.

An asynchronous function, or coroutine function, is defined by prefacing a normal function definition with the extra keyword async. For example:

async def greeting(name):
    print(f'Hello {name}')

If you call such a function, you’ll find that it doesn’t execute in the usual way—in fact, it doesn’t execute at all. Instead, you get an instance of a coroutine object in return. For example:

>>> greeting('Guido')
<coroutine object greeting at 0x104176dc8>
>>>

To make the function run, it must execute under the supervision of other code. A common option is asyncio. For example:

>>> import asyncio
>>> asyncio.run(greeting('Guido'))
Hello Guido
>>>

This example brings up the most important feature of asynchronous functions—that they never execute on their own. Some kind of manager or library code is always required for their execution. It’s not necessarily asyncio as shown, but something is always involved in making async functions run.

Aside from being managed, an asynchronous function evaluates in the same manner as any other Python function. Statements run in order and all of the usual control-flow features work. If you want to return a result, use the usual return statement. For example:

async def make_greeting(name):
    return f'Hello {name}'

The value given to return is returned by the outer run() function used to execute the async function. For example:

>>> import asyncio
>>> a = asyncio.run(make_greeting('Paula'))
>>> a
'Hello Paula'
>>>

Async functions can call other async functions using an await expression like this:

async def make_greeting(name):
    return f'Hello {name}'

async def main():
    for name in ['Paula', 'Thomas', 'Lewis']:
        a = await make_greeting(name)
        print(a)

# Run it. Will see greetings for Paula, Thomas, and Lewis
asyncio.run(main())

Use of await is only valid within an enclosing async function definition. It’s also a required part of making async functions execute. If you leave off the await, you’ll find that the code breaks.

The requirement of using await hints at a general usage issue with asynchronous functions. Namely, their different evaluation model prevents them from being used in combination with other parts of Python. Specifically, it is never possible to write code that calls an async function from a non-async function:

async def twice(x):
    return 2 * x

def main():
    print(twice(2))         # Error. Doesn't execute the function
    print(await twice(2))   # Error. Can't use await here.

Combining async and non-async functionality in the same application is a complex topic, especially if you consider some of the programming techniques involving higher-order functions, callbacks, and decorators. In most cases, support for asynchronous functions has to be built as a special case.

Python does precisely this for the iterator and context manager protocols. For example, an asynchronous context manager can be defined using __aenter__() and __aexit__() methods on a class like this:

class AsyncManager(object):
    def __init__(self, x):
        self.x = x

    async def yow(self):
        pass

    async def __aenter__(self):
        return self

    async def __aexit__(self, ty, val, tb):
        pass

Note that these methods are async functions and can thus execute other async functions using await. To use such a manager, you must use the special async with syntax that is only legal within an async function:

# Example use
async def main():
    async with AsyncManager(42) as m:
         await m.yow()

asyncio.run(main())

A class can similarly define an async iterator by defining methods __aiter__() and __anext__(). These are used by the async for statement which also may only appear inside an async function.

From a practical point of view, an async function behaves exactly the same as a normal function—it’s just that it has to execute within a managed environment such as asyncio. Unless you’ve made a conscious decision to work in such an environment, you should move along and ignore async functions. You’ll be a lot happier.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.