DMarlon Studio

Lesson 19 · 19 of 20

asyncio with async and await · Python

Asynchronous Python with asyncio: coroutines with async and await, gather to run tasks together, sleep(0) and wait_for with a timeout.

Python · asyncio with async and await in DMarlon Studio: the example's code and its output in the terminal

A function defined with `async def` is a coroutine: calling it does not run it, it returns an object that the asyncio event loop will run. asyncio.run(main()) creates that loop, runs main() and closes the loop when it finishes.

`await` waits for another coroutine and hands over control in the meantime. check_stock fakes a warehouse query with await asyncio.sleep(...): while Madrid is busy, the loop serves Bilbao and Sevilla. That is why the three questions go out back to back and the answers arrive fastest first: Bilbao (0.01 s), Sevilla, then Madrid. The whole thing takes as long as the slowest one, not the sum.

`asyncio.gather()` starts several coroutines together and waits for all of them. Even if they finish in a different order, it returns the results in the order they were passed, so results starts with Madrid.

await asyncio.sleep(0) waits for nothing: it just gives control back to the loop. With it, packer and printer take turns on each job. That is the asyncio model: a single thread that only switches tasks at an await, so the order is predictable.

`asyncio.wait_for(..., timeout=0.05)` sets a time limit: the one-second operation is cancelled and TimeoutError is raised, which the program catches. asyncio shines when there is a lot of waiting on the network or disk; for heavy calculations it does not make anything faster.

Code · main.py

import asyncio# Three warehouses answer a stock query; each one takes a different timeDELAY = {"Madrid": 0.03, "Bilbao": 0.01, "Sevilla": 0.02}STOCK = {"Madrid": 12, "Bilbao": 0, "Sevilla": 7}async def check_stock(city, product):    print(f"asking {city} about {product}...")    await asyncio.sleep(DELAY[city])         # waits without blocking the rest    print(f"  {city} answered")    return city, STOCK[city]async def worker(name, jobs):    for job in jobs:        print(f"{name}: {job}")        await asyncio.sleep(0)               # hand control back to the loopasync def main():    # gather runs them at the same time and keeps the order of the results    results = await asyncio.gather(*(check_stock(c, "chair") for c in DELAY))    print("Results:", results)    # Two coroutines taking turns: each await is a switch point    await asyncio.gather(worker("packer", ["box #1", "box #2"]),                         worker("printer", ["label #1", "label #2"]))    try:                                     # give up if it takes too long        await asyncio.wait_for(asyncio.sleep(1), timeout=0.05)    except TimeoutError:        print("Slow supplier: timed out")asyncio.run(main())

Output · python3 main.py

asking Madrid about chair...
asking Bilbao about chair...
asking Sevilla about chair...
  Bilbao answered
  Sevilla answered
  Madrid answered
Results: [('Madrid', 12), ('Bilbao', 0), ('Sevilla', 7)]
packer: box #1
printer: label #1
packer: box #2
printer: label #2
Slow supplier: timed out

Key points

  • async def defines a coroutine; await waits without blocking.
  • asyncio.run() starts the event loop.
  • gather runs several at once and keeps the results in order.
  • Tasks only switch at an await.
  • wait_for cancels whatever goes over the time limit.

Exercise

Add a fourth warehouse, Valencia, with a 0.005 s delay and 3 units, and check where it answers and where it appears in results.