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.
- Explain that calling an
async deffunction creates a coroutine object, andawaitruns it - Await
asyncio.sleepand other coroutines, and read the loop's clock - Recognise the forgotten-
awaitbug and its error
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.
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.
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()))
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.
async defmakes a coroutine function; calling it creates a coroutine;awaitruns it.- Each
awaitis 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.