asyncio runs thousands of I/O tasks on one thread by switching between them at every await. It's cooperative multitasking: fast and lock-free, as long as nobody blocks the loop.
Coroutines, tasks & futures
| Object | What it is | Created by |
|---|---|---|
| Coroutine | The paused computation returned by calling an async def function. Does nothing until awaited or wrapped in a task. | coro = fetch() |
| Task | A coroutine scheduled on the loop — it runs concurrently with the code that created it. | asyncio.create_task(coro), TaskGroup.create_task |
| Future | A low-level placeholder for a result; tasks are futures. Rarely created by hand. | loop.create_future() |
import asyncio
async def greet(name):
await asyncio.sleep(0) # yield control to the loop once
return f"hi {name}"
coro = greet("asha")
print(type(coro).__name__) # coroutine ← nothing has run yet
print(asyncio.run(coro)) # hi asha ← run() starts a loop, runs it, closes itSequential awaits vs concurrent tasks
import asyncio
async def job(name, delay, log):
log.append(f"start {name}")
await asyncio.sleep(delay)
log.append(f"end {name}")
async def sequential():
log = []
await job("a", 0.02, log) # waits for a to finish…
await job("b", 0.01, log) # …before b even starts
return log
async def concurrent():
log = []
t1 = asyncio.create_task(job("a", 0.02, log)) # both scheduled now
t2 = asyncio.create_task(job("b", 0.01, log))
await t1
await t2
return log
print(asyncio.run(sequential())) # ['start a', 'end a', 'start b', 'end b']
print(asyncio.run(concurrent())) # ['start a', 'start b', 'end b', 'end a']gather
import asyncio
async def fetch(i):
await asyncio.sleep(0.01 * (3 - i)) # later items finish first…
if i == 1:
raise ValueError(f"item {i} failed")
return i * 10
async def main():
results = await asyncio.gather(*(fetch(i) for i in range(3)), return_exceptions=True)
print(results) # [0, ValueError('item 1 failed'), 20] ← …but order = input order
try:
await asyncio.gather(fetch(0), fetch(1))
except ValueError as e:
print("first error:", e) # first error: item 1 failed
asyncio.run(main())TaskGroup — structured concurrency (3.11+)
async with asyncio.TaskGroup() waits for every task created in it. If one fails, the others are cancelled and all errors are raised together as an ExceptionGroup — no orphaned tasks.
import asyncio
async def worker(n):
await asyncio.sleep(0.01)
if n == 2:
raise KeyError(n)
return n
async def main():
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(worker(n)) for n in range(2)]
print([t.result() for t in tasks]) # [0, 1] ← all done when the block exits
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(worker(2))
slow = tg.create_task(asyncio.sleep(10))
except* KeyError as eg:
print(type(eg).__name__, len(eg.exceptions)) # ExceptionGroup 1
print(slow.cancelled()) # True ← the sibling was cancelled
asyncio.run(main())Timeouts & cancellation
import asyncio
async def main():
try:
async with asyncio.timeout(0.01): # 3.11+
await asyncio.sleep(1)
except TimeoutError:
print("timeout block") # timeout block
try:
await asyncio.wait_for(asyncio.sleep(1), timeout=0.01)
except TimeoutError:
print("wait_for") # wait_for
asyncio.run(main())import asyncio
async def worker(log):
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
log.append("cleaning up")
raise # always re-raise CancelledError
async def main():
log = []
t = asyncio.create_task(worker(log))
await asyncio.sleep(0) # let it start
t.cancel() # throws CancelledError into it at its await
try:
await t
except asyncio.CancelledError:
log.append("cancelled")
print(log, t.cancelled()) # ['cleaning up', 'cancelled'] True
asyncio.run(main())Queues & semaphores
import asyncio
async def consumer(q, out):
while True:
item = await q.get()
out.append(item * item)
q.task_done()
async def main():
q = asyncio.Queue(maxsize=2) # put() waits when full
out = []
workers = [asyncio.create_task(consumer(q, out)) for _ in range(2)]
for i in range(5): # the producer
await q.put(i)
await q.join() # every item processed
for w in workers:
w.cancel()
await asyncio.gather(*workers, return_exceptions=True)
print(sorted(out)) # [0, 1, 4, 9, 16]
asyncio.run(main())import asyncio
async def main():
sem = asyncio.Semaphore(3) # at most 3 "requests" in flight
active = peak = 0
async def fetch(i):
nonlocal active, peak
async with sem:
active += 1
peak = max(peak, active)
await asyncio.sleep(0.01)
active -= 1
return i
results = await asyncio.gather(*(fetch(i) for i in range(10)))
print(results == list(range(10)), peak) # True 3
asyncio.run(main())Blocking code: asyncio.to_thread
A blocking call (time.sleep, requests.get, heavy CPU) freezes the whole loop — every other task stops. Push it to a worker thread:
import asyncio, time
def blocking_io(x):
time.sleep(0.01) # a blocking library call
return x * 2
async def main():
results = await asyncio.gather(
asyncio.to_thread(blocking_io, 1), # runs in the default thread pool
asyncio.to_thread(blocking_io, 2),
)
print(results) # [2, 4]
asyncio.run(main())Async iterators & context managers
import asyncio
from contextlib import asynccontextmanager
async def ticker(n): # async generator
for i in range(n):
await asyncio.sleep(0)
yield i
log = []
@asynccontextmanager
async def connection(name): # async with → __aenter__ / __aexit__
log.append(f"open {name}")
try:
yield name.upper()
finally:
log.append(f"close {name}")
async def main():
squares = [i * i async for i in ticker(4)] # async comprehension
print(squares) # [0, 1, 4, 9]
async with connection("db") as conn:
log.append(f"using {conn}")
print(log) # ['open db', 'using DB', 'close db']
asyncio.run(main())Common mistakes
import asyncio
async def save():
return "saved"
async def main():
result = save() # forgot await → just a coroutine object
print(type(result).__name__) # coroutine
result.close() # (silences "coroutine was never awaited")
print(await save()) # saved
asyncio.run(main())| Mistake | Symptom | Fix |
|---|---|---|
Forgetting await | RuntimeWarning: coroutine … was never awaited; nothing happens | await it or create a task |
time.sleep() / requests in a coroutine | All tasks freeze | await asyncio.sleep(), async libraries, or asyncio.to_thread |
| CPU-heavy loop in a coroutine | Latency spikes for every other task | loop.run_in_executor(ProcessPoolExecutor(), …) |
Calling asyncio.run inside a running loop | RuntimeError | await the coroutine instead |
| Fire-and-forget tasks without a reference | Task silently disappears; errors never seen | Keep references / use TaskGroup |
Sync vs async at a glance
| Sync | Async |
|---|---|
def f(): | async def f(): |
f() | await f() |
time.sleep(1) | await asyncio.sleep(1) |
for x in it: | async for x in ait: |
with cm: | async with cm: |
threading.Lock | asyncio.Lock (only across awaits) |
queue.Queue | asyncio.Queue |
ThreadPoolExecutor.map | asyncio.gather / TaskGroup |
How is asyncio different from threading?
What happens if you call a blocking function inside a coroutine?
gather vs TaskGroup?