Python Central HubSign in

Python Programming / Python Asyncio

Asyncio in Python

Learn asynchronous programming with asyncio - event loop, coroutines, tasks, and concurrency for I/O-bound workloads.

What is asyncio?

asyncio is Python’s built-in library for asynchronous I/O.

It’s designed for workloads that spend lots of time waiting:

  • network requests
  • database calls
  • file I/O (through async libraries)

Async vs threading vs multiprocessing

  • asyncio: single-threaded concurrency using an event loop (great for many I/O tasks)
  • threading: multiple threads (good for I/O, simpler when using blocking libs)
  • multiprocessing: multiple processes (good for CPU-bound)

Mental model

  • async def defines a coroutine.
  • await pauses the coroutine so other tasks can run.
  • The event loop schedules coroutines and resumes them when they’re ready.

Your first coroutine

Python
import asyncio
 
 
async def main():
    print("Hello")
    await asyncio.sleep(1)
    print("World")
 
 
asyncio.run(main())

When asyncio helps

If you do 100 HTTP requests sequentially, you wait 100 times.

With asyncio you can:

  • start many requests
  • efficiently wait for them concurrently

When asyncio won’t help

For CPU-heavy loops, asyncio won’t speed things up.

Use:

  • multiprocessing
  • numpy/vectorization
  • compiled extensions

await is not where concurrency comes from

This is the single most common misreading of asyncio. await means wait here until this finishes. Written in a loop, it is exactly as sequential as ordinary code:

diagram Diagram mermaid

Measured, five coroutines each sleeping 0.2 s:

timespeedup
for i in range(5): await work(i)1.005 s1.0×
await asyncio.gather(*[work(i) for i in range(5)])0.215 s4.7×

Concurrency comes from handing the loop several things at once — gather, create_task, or a TaskGroup. An await on its own just yields control until that one operation is done.

Calling a coroutine function does nothing

Python
import asyncio, inspect
 
async def work(name):
    await asyncio.sleep(0.1)
    return name
 
c = work("a")                     # no code has run yet
print(type(c).__name__)           # coroutine
print(inspect.iscoroutine(c))     # True
print(asyncio.run(c))             # 'a'   <- now it runs

A coroutine object is a description of work. Forgetting to await one is why RuntimeWarning: coroutine ... was never awaited exists — the call looked like it did something and did not.

create_task schedules, a bare coroutine waits to be asked

Python
log = []
 
async def note(n):
    log.append(f"{n} started")
    await asyncio.sleep(0.05)
 
t1 = asyncio.create_task(note("task"))   # handed to the loop right away
co = note("coro")                        # just an object; nothing scheduled
await asyncio.sleep(0.01)
print(log)                               # ['task started']   <- only one of them began

See it move

One thread, one loop. A coroutine runs until it hits an await, hands control back, and the loop picks whoever is ready. Nothing here is parallel — it is interleaved, and that is enough when the work is waiting.

sketch How the event loop interleaves coroutines p5.js
Each coroutine runs until it awaits, then yields to the loop. With gather, three coroutines overlap their waiting; awaited one by one they cannot.

One blocking call freezes everything

Because there is only one thread, any function that blocks without awaiting stops the whole loop — every other coroutine included:

Python
async def good():
    await asyncio.sleep(0.3)      # yields to the loop
 
async def blocker():
    time.sleep(0.3)               # WRONG inside async: nothing yields
two coroutines running togethermeasured
both await asyncio.sleep(0.3)0.310 s — overlapped
both time.sleep(0.3)0.602 s — serialized

gather returns in argument order

Python
res = await asyncio.gather(timed("slow", 0.3), timed("fast", 0.05), timed("mid", 0.15))
print(res)              # ['slow', 'fast', 'mid']   <- the order you passed them
# actual completion order was ['fast', 'mid', 'slow']

Results line up with your arguments, which makes zip(urls, results) safe. If you want results as they arrive, use asyncio.as_completed.

When one of them fails

Python
await asyncio.gather(work("ok"), bad())
# ValueError: boom      <- the first exception propagates immediately
 
await asyncio.gather(work("ok"), bad(), return_exceptions=True)
# ['ok', ValueError('boom')]    <- collected instead of raised

With return_exceptions=True you always get one entry per input, and it is your job to check which entries are exceptions.

Check yourself

quizCheck yourself
  1. Five coroutines each sleep 0.2 s. Awaiting them one per loop iteration took 1.005 s; gather took 0.215 s. What does that show?

    Show answer

    B — await means 'wait here', so concurrency comes from handing several coroutines to the loop at once — A loop of awaits is sequential by definition. gather submits all five, so their waiting overlaps. Everything still runs on one thread.

  2. You write c = work('a') where work is an async def, and never await c. What has happened?

    Show answer

    C — nothing ran; c is a coroutine object and Python warns it was never awaited — Calling an async function builds a coroutine object describing the work. inspect.iscoroutine(c) is True and no body has executed until it is awaited or scheduled.

  3. Two coroutines that both call time.sleep(0.3) took 0.602 s, while two that await asyncio.sleep(0.3) took 0.310 s. Why?

    Show answer

    B — time.sleep blocks the single thread the loop runs on, so nothing else can proceed — asyncio is single-threaded. A blocking call never yields control, so the loop cannot switch to the other coroutine. Use asyncio.to_thread for work you cannot make awaitable.

  4. gather is given slow (0.3 s), fast (0.05 s) and mid (0.15 s), in that order. What does it return?

    Show answer

    B — results in argument order: slow, fast, mid — gather preserves argument order regardless of when each finished, which is what makes zip(inputs, results) correct. Use asyncio.as_completed if you want them as they arrive.

🧪 Try It Yourself

Exercise 1 – Your First Coroutine

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – await asyncio.sleep

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Gather Two Coroutines

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did