BugForge
All lessons
Python · 10/10 6 min

asyncio essentials

Coroutines that never run, gather vs sequential awaits, blocking calls and locks.

Calling a coroutine doesn't run it

async def fetch(id): ...

fetch(1)              # ❌ creates a coroutine object; nothing happens
                      # RuntimeWarning: coroutine 'fetch' was never awaited
await fetch(1)        # ✅

Same shape as a missing await in JavaScript: you get an object, not a result.

Concurrency with gather

results = [await fetch(i) for i in ids]                  # sequential
results = await asyncio.gather(*(fetch(i) for i in ids)) # concurrent, results in input order
results = await asyncio.gather(*tasks, return_exceptions=True)   # don't fail fast

Limit fan-out with a semaphore:

sem = asyncio.Semaphore(10)
async def limited(i):
    async with sem:
        return await fetch(i)

Never block the loop

time.sleep(1)                 # ❌ freezes every task
await asyncio.sleep(1)        # ✅
requests.get(url)             # ❌ blocking I/O inside async code
await asyncio.to_thread(requests.get, url)   # run blocking work in a thread

Tasks

task = asyncio.create_task(worker())   # starts now, runs alongside
...
await task                             # get the result / exception

Keep a reference to tasks you create: an unreferenced task can be garbage-collected before it finishes, and its exception is only logged.

Races still exist

Only one coroutine runs at a time, but any await is a switch point:

if self.balance >= amount:      # check
    await self.audit()          # ← another task can run here
    self.balance -= amount      # act: may overdraw

Hold an asyncio.Lock around check-then-act, or don't await in between.

Entry point

asyncio.run(main()) once, at the top. Calling it inside an already-running loop raises RuntimeError.

Next in Python
Python gotchas