runtime
191 / 250
lesson 191 Async Python · advanced

async and await: coroutines and the event loop

An async function can pause while it waits, letting other work run in the same thread. Calling it doesn't run it; awaiting it does. The event loop decides who runs next.

You'll be able to
  • Explain that calling an async def function creates a coroutine object, and await runs it
  • Await asyncio.sleep and other coroutines, and read the loop's clock
  • Recognise the forgotten-await bug and its error
The idea

Threads overlap waiting by running several things at once. asyncio overlaps waiting in a single thread: a function declared with async def is a coroutine function, and inside it await something means *pause me here until that is ready, and let other code run meanwhile*. An event loop keeps the list of paused coroutines and resumes each one when what it awaits is done. asyncio.run(main()) creates a loop, runs main() to the end and closes the loop.

Python in this page can't call asyncio.run directly, so every example starts with a few lines defining run(main): the same asyncio.run, given an event loop whose clock jumps to the next timer instead of really waiting. Everything after those lines is ordinary asyncio, and on a computer you would write asyncio.run(main()) instead.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Two awaits, one after the other
import asyncio

class BrowserLoop(asyncio.BaseEventLoop):  # a real asyncio loop whose clock jumps instead of waiting
    def __init__(self):
        super().__init__()
        self._clock, self._selector = 0.0, self
    def time(self):
        return self._clock
    def select(self, timeout):
        self._clock += timeout
        return []
    def _process_events(self, events):
        pass
    def _write_to_self(self):
        pass

def run(main):  # on a computer: asyncio.run(main)
    outer = asyncio.events._get_running_loop()
    asyncio.events._set_running_loop(None)
    try:
        return asyncio.run(main, loop_factory=BrowserLoop)
    finally:
        asyncio.events._set_running_loop(outer)

async def fetch_order(order_id, seconds):
    await asyncio.sleep(seconds)
    return {"order_id": order_id, "status": "shipped"}

async def main():
    loop = asyncio.get_running_loop()
    first = await fetch_order("O100282", 0.2)
    second = await fetch_order("O100376", 0.3)
    print(first["order_id"], second["order_id"], round(loop.time(), 2))
    pending = fetch_order("O100918", 0.1)
    print(type(pending).__name__)
    pending.close()

run(main())

Coroutines call coroutines with await, just as functions call functions. The difference is only at the waits: each await is a point where this coroutine may pause. A plain function can't contain await, and await only works inside async def.

example-2.py · A checkout made of async steps
import asyncio

class BrowserLoop(asyncio.BaseEventLoop):  # a real asyncio loop whose clock jumps instead of waiting
    def __init__(self):
        super().__init__()
        self._clock, self._selector = 0.0, self
    def time(self):
        return self._clock
    def select(self, timeout):
        self._clock += timeout
        return []
    def _process_events(self, events):
        pass
    def _write_to_self(self):
        pass

def run(main):  # on a computer: asyncio.run(main)
    outer = asyncio.events._get_running_loop()
    asyncio.events._set_running_loop(None)
    try:
        return asyncio.run(main, loop_factory=BrowserLoop)
    finally:
        asyncio.events._set_running_loop(outer)

async def load_rates():
    await asyncio.sleep(0.15)
    return {"EUR": 0.92, "GBP": 0.79}

async def load_cart(customer_id):
    await asyncio.sleep(0.1)
    return [{"sku": "SPO-11681", "price": 7.49}, {"sku": "HOM-22847", "price": 31.0}]

async def checkout_total(customer_id, currency):
    rates = await load_rates()
    cart = await load_cart(customer_id)
    total = sum(item["price"] for item in cart)
    return round(total * rates[currency], 2)

async def main():
    loop = asyncio.get_running_loop()
    print(await checkout_total("C00077", "EUR"), round(loop.time(), 2))

print(run(main()))
example-3.py · Forgetting await
import asyncio

class BrowserLoop(asyncio.BaseEventLoop):  # a real asyncio loop whose clock jumps instead of waiting
    def __init__(self):
        super().__init__()
        self._clock, self._selector = 0.0, self
    def time(self):
        return self._clock
    def select(self, timeout):
        self._clock += timeout
        return []
    def _process_events(self, events):
        pass
    def _write_to_self(self):
        pass

def run(main):  # on a computer: asyncio.run(main)
    outer = asyncio.events._get_running_loop()
    asyncio.events._set_running_loop(None)
    try:
        return asyncio.run(main, loop_factory=BrowserLoop)
    finally:
        asyncio.events._set_running_loop(outer)

async def order_total(order_id):
    await asyncio.sleep(0.1)
    return 35.84

async def main():
    total = order_total("O100282") + 5.99
    print(total)

run(main())

When to use asyncio: programs that spend their time waiting on many network or disk operations (API clients, crawlers, chat servers), especially when there are hundreds or thousands of them: a coroutine costs far less than a thread. When not to: CPU-heavy work (a coroutine that computes never reaches an await, so nothing else runs: use processes, the Threading & Multiprocessing module). Mistakes to avoid: forgetting await; calling blocking functions such as time.sleep or a synchronous HTTP library inside a coroutine (it freezes every coroutine on the loop); and awaiting independent operations one after another when they could overlap.

Review
  • async def makes a coroutine function; calling it creates a coroutine; await runs it.
  • Each await is a point where the coroutine may pause and let others run.
  • asyncio.run(main()) runs the event loop (run(main()) here); awaits in sequence add their waits up.
  • Next: tasks, which let independent awaits overlap.

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

async and await: coroutines and the event loop — Async Python · Runtime