- 5.1 Function Definitions
- 5.2 Default Arguments
- 5.3 Variadic Arguments
- 5.4 Keyword Arguments
- 5.5 Variadic Keyword Arguments
- 5.6 Functions Accepting All Inputs
- 5.7 Positional-Only Arguments
- 5.8 Names, Documentation Strings, and Type Hints
- 5.9 Function Application and Parameter Passing
- 5.10 Return Values
- 5.11 Error Handling
- 5.12 Scoping Rules
- 5.13 Recursion
- 5.14 The lambda Expression
- 5.15 Higher-Order Functions
- 5.16 Argument Passing in Callback Functions
- 5.17 Returning Results from Callbacks
- 5.18 Decorators
- 5.19 Map, Filter, and Reduce
- 5.20 Function Introspection, Attributes, and Signatures
- 5.21 Environment Inspection
- 5.22 Dynamic Code Execution and Creation
- 5.23 Asynchronous Functions and await
- 5.24 Final Words: Thoughts on Functions and Composition
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.