async, await and the Event Loop
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Threads and processes use the operating system to do many things at once. Asyncio does it inside one thread: tasks wait their turn and hand control back whenever they wait.
Coroutines and asyncio.run
Coroutine is the object made by calling an async def function. It is a paused piece of work that runs only when something awaits it or the event loop schedules it.
A function made with async def is a coroutine function. Calling it does not run the body; it gives back a coroutine object.
import asyncio
async def greet(name): return f"hello {name}"
print(asyncio.run(greet("Asha")))asyncio.run(...) runs that object to the end and returns its result.
Example 1 · A coroutine object does nothing until it runs
import asyncio
async def total(): return 5 + 6
coro = total()print(asyncio.iscoroutine(coro))print(asyncio.run(coro))True 11
What this code does
total()returns a coroutine object. The5 + 6has not been calculated yet.asyncio.runruns it and gives back11.
Call asyncio.run once, at the top level of the program. Everything else is awaited inside it.
await pauses and gives the loop a turn
await pauses the current coroutine until the awaited thing is ready, and lets the event loop run other work in the meantime. It works only inside async def.
await waits for something that takes time, like a pause, a reply or another coroutine. While this coroutine waits, the loop can run other work.
import asyncio
async def price(item): await asyncio.sleep(0.1) # a pause that does not block return 100
async def main(): print(await price("pen"))
asyncio.run(main())You may use await only inside async def.
Example 2 · Awaiting in a row
import asyncio
async def get_price(item): await asyncio.sleep(0.05) # a slow lookup return {"pen": 20, "book": 150}[item]
async def basket_total(items): total = 0 for item in items: total += await get_price(item) return total
print(asyncio.run(basket_total(["pen", "book", "pen"])))190
What this code does
- Each
awaitfinishes before the next line runs, so this is the same order as normal code. - One coroutine awaits another, which is how async programs are built up.
asyncio.sleep stands in for a slow call. Real programs await a network reply or a database answer in the same place.
The event loop
Event loop is the scheduler behind asyncio. It keeps a list of waiting and ready coroutines and runs each ready one until its next await, all on a single thread.
One thread runs all your coroutines. The event loop picks a ready coroutine, runs it until its next await, then picks another.
It is one cook switching between pots, not many cooks.
Example 3 · Two coroutines take turns
import asyncio
async def step(name, log): for i in (1, 2): log.append(f"{name}{i}") await asyncio.sleep(0) # give the loop a turn
async def main(): log = [] await asyncio.gather(step("A", log), step("B", log)) print(log)
asyncio.run(main())['A1', 'B1', 'A2', 'B2']
What this code does
asyncio.sleep(0)pauses for no time, but hands the turn to the other coroutine.- A runs until its first await, then B, then A again. That fixed turn order is how the loop is built.
gatherruns both together; the next lesson explains it.
Switching happens only at an await. That is why async code needs no locks for simple updates, and why one long calculation freezes everything.
A blocking call freezes the loop
Blocking call is a call that holds the thread until it finishes, such as time.sleep or a normal network read. In async code it stops every other coroutine.
Inside a coroutine, time.sleep or a normal slow function blocks the one thread, so no other coroutine can run. await asyncio.sleep does not block.
import asyncioimport time
async def bad(): time.sleep(1) # blocks every coroutine
async def good(): await asyncio.sleep(1) # others can run meanwhileUse awaitable calls, or move blocking work to a thread.
Example 4 · Forgetting await
import asyncio
async def fetch(): return "data"
async def main(): result = fetch() # no await print(type(result).__name__) result.close() # we never run it print(await fetch())
asyncio.run(main())coroutine data
What this code does
- Without
await,resultis a coroutine object, not the string. - Python warns "coroutine was never awaited" when this happens. We close it here to avoid the warning.
- With
await, the body runs and givesdata.
A blocked loop is the main async bug in real services. Everything seems to hang until the call ends.
Next step
Check your understanding
10 quick questions