Python async/await
last modified October 30, 2026 Python 3.15
In this article we show how to use the async/await keywords in Python.
With asynchronous programming, we can run many tasks concurrently in a single
thread. While one task waits for a network response, a timer, or a database,
other tasks can run. The async and await keywords
make asynchronous code look almost like regular sequential code.
Python coroutine
A coroutine is a function that can be paused and resumed. Coroutines are used
in cooperative multitasking: a coroutine runs until it voluntarily gives up
control at an await expression. A coroutine is declared with the
async def syntax.
The async/await keywords
The async/await syntax was introduced in Python 3.5 (PEP 492);
since Python 3.7, async and await are reserved
keywords. The async keyword is used to create a coroutine. The
await keyword suspends the execution of the current coroutine
until the awaited object completes and returns its result. The
await keyword can only be used inside an async
function.
Coroutines need an event loop to run. The event loop is provided by the
asyncio module of the standard library; see
Python asyncio for more details on the module.
Python async/await first example
The following is a simple program which uses the async keyword.
import asyncio
async def mul(x, y):
return x * y
coro = mul(5, 5)
print(type(coro))
res = asyncio.run(coro)
print(res)
The program creates and runs a coroutine.
async def mul(x, y):
return x * y
A coroutine function is a function declared with the async
modifier.
coro = mul(5, 5) print(type(coro))
Calling a coroutine function does not run its body. It returns a coroutine object.
res = asyncio.run(coro)
The asyncio.run function creates an event loop, runs the
coroutine until it completes, closes the loop, and returns the result. It is
the usual entry point of an asynchronous program.
$ python first_coroutine.py <class 'coroutine'> 25
Awaiting coroutines
Inside a coroutine, we run other coroutines with await.
import asyncio
async def add(x, y):
return x + y
async def get_results():
res1 = await add(3, 4)
res2 = await add(8, 5)
print(res1, res2)
asyncio.run(get_results())
The get_results coroutine awaits two calls to add.
Each await returns the value returned by the coroutine. The
second call starts only after the first one completes.
$ python await_coroutines.py 7 13
Sequential vs concurrent execution
Awaiting coroutines one after another runs them sequentially. To run them
concurrently, we use asyncio.gather. With
asyncio.sleep, we create a coroutine that finishes after the
specified number of seconds; it simulates a long-running task.
import asyncio
import time
async def task(tid, n):
await asyncio.sleep(n)
print(f'task {tid} finished after {n} s')
async def sequential():
await task(1, 3)
await task(2, 2)
await task(3, 1)
async def concurrent():
await asyncio.gather(task(1, 3), task(2, 2), task(3, 1))
for main in (sequential, concurrent):
t1 = time.perf_counter()
asyncio.run(main())
t2 = time.perf_counter()
print(f'{main.__name__}: {t2 - t1:0.2f} seconds')
We run three tasks that take three, two, and one second.
async def sequential():
await task(1, 3)
await task(2, 2)
await task(3, 1)
Each task starts after the previous one completes. The total time is the sum of the individual times.
async def concurrent():
await asyncio.gather(task(1, 3), task(2, 2), task(3, 1))
The gather function runs all three coroutines at the same time.
While one task sleeps, the event loop runs the others. The total time is the
time of the longest task, and the tasks finish in the order of their
durations.
$ python sequential_concurrent.py task 1 finished after 3 s task 2 finished after 2 s task 3 finished after 1 s sequential: 6.01 seconds task 3 finished after 1 s task 2 finished after 2 s task 1 finished after 3 s concurrent: 3.00 seconds
Results of gather
The gather function returns a list of results. The results are
in the order of the arguments, not in the order in which the coroutines
finished.
import asyncio
async def fetch_price(product, delay):
await asyncio.sleep(delay)
print(f'got {product}')
return f'{product}: {delay * 10:.2f} EUR'
async def main():
prices = await asyncio.gather(
fetch_price('apples', 0.3),
fetch_price('pears', 0.1),
fetch_price('plums', 0.2),
)
print(prices)
asyncio.run(main())
We simulate fetching three prices with different delays. The pears arrive first, but the list of prices keeps the order of the calls.
$ python gather_results.py got pears got plums got apples ['apples: 3.00 EUR', 'pears: 1.00 EUR', 'plums: 2.00 EUR']
Creating tasks
The asyncio.create_task function schedules a coroutine to run in
the background and returns a task object. The current coroutine can do other
work and await the task later.
import asyncio
import time
async def download(name, n):
print(f'{name}: started')
await asyncio.sleep(n)
print(f'{name}: done')
return n
async def main():
t1 = time.perf_counter()
task = asyncio.create_task(download('report', 2))
for i in range(3):
print(f'main: working {i}')
await asyncio.sleep(0.5)
print(f'task done: {task.done()}')
res = await task
print(f'result: {res}, elapsed: {time.perf_counter() - t1:0.2f} s')
asyncio.run(main())
We start a download task and then do some work in the main
coroutine.
task = asyncio.create_task(download('report', 2))
The task does not start immediately; it starts when main gives
up control at its first await.
res = await task
Awaiting the task returns its result. Since the download ran while
main was working, the total time is two seconds, not three and a
half.
$ python create_task.py main: working 0 report: started main: working 1 main: working 2 task done: False report: done result: 2, elapsed: 2.00 s
Task groups
The asyncio.TaskGroup (Python 3.11) is the recommended way to run
a group of tasks. The async with block waits for all tasks to
finish. If any task fails, the remaining tasks are cancelled and the errors
are raised as an ExceptionGroup.
import asyncio
async def square(n):
await asyncio.sleep(n / 10)
return n * n
async def failing(n):
await asyncio.sleep(n / 10)
raise ValueError(f'cannot process {n}')
async def main():
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(square(n)) for n in range(1, 6)]
print([task.result() for task in tasks])
try:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(square(n)) for n in range(1, 6)]
tg.create_task(failing(2))
except* ValueError as eg:
print(f'errors: {eg.exceptions}')
print([task.cancelled() for task in tasks])
asyncio.run(main())
In the first group, all tasks succeed and we read their results with the
result method.
try:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(square(n)) for n in range(1, 6)]
tg.create_task(failing(2))
except* ValueError as eg:
print(f'errors: {eg.exceptions}')
In the second group, one task fails after 0.2 seconds. The tasks that have
not finished by then are cancelled. The except* clause handles
the exceptions from the group.
$ python task_group.py
[1, 4, 9, 16, 25]
errors: (ValueError('cannot process 2'),)
[False, False, True, True, True]
Cancelling a task group
Python 3.15 adds the TaskGroup.cancel method. It cancels all
unfinished tasks of the group, and the async with block exits
without raising an exception. A typical use is to stop when the first result
is available.
import asyncio
async def query(mirror, delay, results, tg):
await asyncio.sleep(delay)
results.append(mirror)
print(f'{mirror} answered after {delay} s')
tg.cancel()
async def main():
results = []
try:
async with asyncio.TaskGroup() as tg:
for mirror, delay in [('eu', 0.3), ('us', 0.1), ('asia', 0.2)]:
tg.create_task(query(mirror, delay, results, tg))
except* AttributeError as eg:
print(f'AttributeError: {eg.exceptions[0]}')
print(f'answers: {results}')
asyncio.run(main())
We query three mirrors and keep the first answer.
results.append(mirror)
print(f'{mirror} answered after {delay} s')
tg.cancel()
The fastest task cancels the group, so the slower mirrors never answer.
except* AttributeError as eg:
print(f'AttributeError: {eg.exceptions[0]}')
We run the program with Python 3.15 and with Python 3.14. On 3.14, the
method does not exist; the AttributeError raised inside the task
makes the group cancel the other tasks and is reported in an exception group.
$ python first_result.py us answered after 0.1 s answers: ['us'] $ python3.14 first_result.py us answered after 0.1 s AttributeError: 'TaskGroup' object has no attribute 'cancel' answers: ['us']
Handling exceptions in gather
By default, gather raises the first exception that occurs in
any of the coroutines. With return_exceptions=True, exceptions
are returned in the list of results.
import asyncio
async def parse(text):
await asyncio.sleep(0.1)
return int(text)
async def main():
data = ['12', '7', 'x', '42']
try:
await asyncio.gather(*(parse(item) for item in data))
except ValueError as e:
print(f'gather failed: {e}')
res = await asyncio.gather(*(parse(item) for item in data),
return_exceptions=True)
print(res)
asyncio.run(main())
We parse four strings into integers; one of them is invalid.
res = await asyncio.gather(*(parse(item) for item in data),
return_exceptions=True)
The exception object takes the place of the failed result, and the other results are kept.
$ python gather_exceptions.py
gather failed: invalid literal for int() with base 10: 'x'
[12, 7, ValueError("invalid literal for int() with base 10: 'x'"), 42]
Timeouts
The asyncio.timeout context manager (Python 3.11) cancels the
code inside the block if it takes too long, and raises a
TimeoutError. The asyncio.wait_for function does the
same for a single awaitable.
import asyncio
async def slow_operation():
await asyncio.sleep(5)
return 'finished'
async def main():
try:
async with asyncio.timeout(1):
res = await slow_operation()
print(res)
except TimeoutError:
print('the operation timed out')
try:
res = await asyncio.wait_for(slow_operation(), timeout=0.5)
except TimeoutError:
print('wait_for timed out')
asyncio.run(main())
The slow_operation coroutine takes five seconds, but we allow it
only one second, and half a second the second time.
$ python timeouts.py the operation timed out wait_for timed out
Asynchronous generators and async for
An async def function that contains yield is an
asynchronous generator. We iterate over it with async for.
Asynchronous comprehensions use async for as well.
import asyncio
async def ticker(n, delay):
for i in range(n):
await asyncio.sleep(delay)
yield i
async def main():
async for tick in ticker(3, 0.2):
print(f'tick {tick}')
squares = [tick * tick async for tick in ticker(5, 0.1)]
print(squares)
asyncio.run(main())
The ticker generator awaits between the values it yields.
squares = [tick * tick async for tick in ticker(5, 0.1)]
An asynchronous list comprehension collects the values into a list.
$ python async_generator.py tick 0 tick 1 tick 2 [0, 1, 4, 9, 16]
Asynchronous context managers and async with
The async with statement works with asynchronous context
managers, whose setup and cleanup can await. A class implements the
__aenter__ and __aexit__ methods; the
asynccontextmanager decorator creates one from an asynchronous
generator.
import asyncio
from contextlib import asynccontextmanager
class Connection:
async def __aenter__(self):
await asyncio.sleep(0.1)
print('connected')
return self
async def __aexit__(self, exc_type, exc, tb):
await asyncio.sleep(0.1)
print('disconnected')
async def query(self, sql):
await asyncio.sleep(0.1)
return f'result of {sql!r}'
@asynccontextmanager
async def transaction(con):
print('begin')
yield con
print('commit')
async def main():
async with Connection() as con:
async with transaction(con):
print(await con.query('SELECT 1'))
asyncio.run(main())
The Connection class simulates connecting to and disconnecting
from a database.
@asynccontextmanager
async def transaction(con):
print('begin')
yield con
print('commit')
The code before yield runs when the block is entered, the code
after it when the block is exited.
$ python async_context.py connected begin result of 'SELECT 1' commit disconnected
Blocking code in coroutines
The event loop can switch tasks only at await expressions. A
blocking call, such as time.sleep or a synchronous HTTP request,
stops all tasks. The asyncio.to_thread function runs blocking
code in a separate thread.
import asyncio
import time
def blocking_io(n):
time.sleep(n)
return n
async def heartbeat():
for _ in range(4):
print(f'heartbeat {time.perf_counter() - start:0.1f}')
await asyncio.sleep(0.5)
async def bad():
blocking_io(1.5)
async def good():
await asyncio.to_thread(blocking_io, 1.5)
async def main(worker):
await asyncio.gather(heartbeat(), worker())
for worker in (bad, good):
print(f'--- {worker.__name__}')
start = time.perf_counter()
asyncio.run(main(worker))
The heartbeat coroutine prints the elapsed time every half
second while a worker runs.
async def bad():
blocking_io(1.5)
The bad worker calls the blocking function directly. The
heartbeat stops for one and a half seconds.
async def good():
await asyncio.to_thread(blocking_io, 1.5)
The good worker runs the function in a thread, and the heartbeat
keeps ticking.
$ python blocking.py --- bad heartbeat 0.0 heartbeat 1.5 heartbeat 2.0 heartbeat 2.5 --- good heartbeat 0.0 heartbeat 0.5 heartbeat 1.0 heartbeat 1.5
Python Playwright async example
Many libraries have support for asynchronous programming. The Playwright
library allows to automate browsers both in synchronous and asynchronous
modes. It is installed with pip install playwright, and the
browsers with playwright install.
import asyncio
from playwright.async_api import async_playwright
async def get_title(browser, url):
page = await browser.new_page()
await page.goto(url)
title = await page.title()
await page.close()
return title
async def main():
urls = ['https://example.com', 'https://www.python.org',
'https://docs.python.org/3/']
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
titles = await asyncio.gather(*(get_title(browser, url)
for url in urls))
for url, title in zip(urls, titles):
print(f'{url}: {title}')
await browser.close()
asyncio.run(main())
In the example, we get the titles of three web pages with Playwright. We use the async API.
titles = await asyncio.gather(*(get_title(browser, url)
for url in urls))
Each page is opened in a new tab of the same browser, and the pages are loaded concurrently. See Python Playwright.
$ python page_title.py https://example.com: Example Domain https://www.python.org: Welcome to Python.org https://docs.python.org/3/: Python 3.15.2 documentation
Source
Coroutines and tasks - Python documentation
In this article we have worked with async/await keywords in Python.
Author
List all Python tutorials.