Chapters
Python114 chapters

Beyond the basicsChapter 90 of 114

Async Basics

Do other work while waiting, without threads.

The problem

Most programs spend their time waiting — for a network reply, a database, a file. A normal program waits and does nothing. Ten requests taking one second each take ten seconds:

Python
import time

def fetch(name):
    time.sleep(1)
    return name

start = time.perf_counter()
results = [fetch(n) for n in range(10)]
print(f"{time.perf_counter() - start:.0f} seconds")

Output, from a real run elsewhere

10 seconds

Async lets one thread start all ten and handle each reply as it arrives.

async def and await

An async def function is a coroutine. Calling it does not run it; it returns an object you must await:

Python
import asyncio

async def greet():
    return "hello"

coro = greet()
print(type(coro).__name__)
print(asyncio.run(coro))

Output

coroutine
hello

asyncio.run() starts the event loop, runs the coroutine to completion, and shuts the loop down. It is the entry point from ordinary code.

await hands control back

Python
import asyncio

async def work(name, seconds):
    print("starting", name)
    await asyncio.sleep(seconds)
    print("finished", name)
    return name

async def main():
    await work("a", 1)
    await work("b", 1)

asyncio.run(main())

Output

starting a
finished a
starting b
finished b

That is still two seconds — awaiting one after the other is just waiting in order. The win comes from starting them together.

Running things at the same time

Python
import asyncio

async def work(name, seconds):
    await asyncio.sleep(seconds)
    return name

async def main():
    results = await asyncio.gather(
        work("a", 1),
        work("b", 1),
        work("c", 1),
    )
    print(results)

asyncio.run(main())

Output

['a', 'b', 'c']

Three one-second waits, one second total. gather returns the results in the order you passed them, not the order they finished.

Tasks

create_task schedules a coroutine immediately and gives you a handle:

Python
import asyncio

async def work(name):
    await asyncio.sleep(0.1)
    return name

async def main():
    task = asyncio.create_task(work("a"))
    print("task is running while we do other things")
    print(await task)

asyncio.run(main())

Output

task is running while we do other things
a

await only inside async def

Python
def broken():
    await asyncio.sleep(1)   # SyntaxError

And an async def cannot be called like a normal function — you get a coroutine object and a warning that it was never awaited. Forgetting await is the most common async bug, and the symptom is code that appears to do nothing.

It only helps with waiting

Async is concurrency, not parallelism. There is still one thread, so computation gains nothing:

Python
import asyncio

async def compute():
    return sum(range(10_000_000))   # blocks everything while it runs

async def main():
    await asyncio.gather(compute(), compute())

asyncio.run(main())

For processor-bound work use multiprocessing. For blocking calls you cannot avoid, hand them to a thread:

Python
import asyncio

async def main():
    result = await asyncio.to_thread(open("file.txt").read)
    print(len(result))

Everything in the chain has to be async

A blocking call inside a coroutine blocks the whole loop, which quietly removes the benefit:

Python
import asyncio, time

async def bad():
    time.sleep(1)          # blocks the event loop

async def good():
    await asyncio.sleep(1) # yields to the loop

This is why async libraries come in pairs: requests blocks, httpx and aiohttp do not.

When to use it

Use it for lots of concurrent waiting — a web server, a scraper, a client making many API calls. For a script that does three things in order, plain synchronous code is simpler and just as fast.

Test yourself

2 questions

What does calling an async function without await give you?

Show the answer

A coroutine object that never runs — Forgetting await is the most common async bug, and the symptom is code that seems to do nothing.

What kind of work does async speed up?

Show the answer

Waiting, such as network or disk — It is concurrency, not parallelism. For processor-bound work use multiprocessing.

Next chapter

NumPy Intro

Arrays that do arithmetic on every element at once.