C.W.K.
Stream
Lesson 01 of 05 · published

asyncio Basics — async, await, the Event Loop

~22 min · asyncio, async, await, coroutine

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Three concurrency models — and asyncio is the modern default for I/O

Python has three: asyncio (single-threaded cooperative concurrency), threading (multiple threads, the GIL limits CPU parallelism), multiprocessing (multiple processes, true parallelism). For I/O-bound work — the network, the filesystem, anywhere you'd otherwise wait — asyncio is the modern default. cwkPippa is asyncio top to bottom.

async def and await

async def f(): defines a coroutine. Calling it doesn't run the body — it returns a coroutine object. await coro runs the coroutine to its next suspension or completion. The await keyword is the magic: it tells the event loop "you can run something else while we're waiting here."

asyncio.run — the entry point

asyncio.run(main()) creates an event loop, runs the coroutine until it returns, then shuts the loop down. This is how you start an async program from a regular Python script. From inside an async function, you don't call asyncio.run — you just await.

Tasks — running coroutines concurrently

await coro waits for ONE coroutine. To run several concurrently, wrap each in a Task with asyncio.create_task(coro). await asyncio.gather(*tasks) waits for all of them. asyncio.TaskGroup (3.11+) is the modern shape — block-scoped task management with proper exception handling.

Principle: "Async all the way" is a real constraint. You can't await inside a regular def. You can't call an async function without await (you'll get a coroutine object back, not a result). Once you decide a function is async, all its callers up the chain become async too.

Code

Hello async world·python
import asyncio

async def hello():
    print("hi")
    await asyncio.sleep(0.5)              # 'sleep' yields control to the loop
    print("world")

# From a regular script — start the event loop
asyncio.run(hello())
# hi
# (half-second pause)
# world
Tasks vs sequential — concurrency wins·python
import asyncio
import time

async def fetch(name, delay):
    print(f"{name}: starting")
    await asyncio.sleep(delay)
    print(f"{name}: done")
    return name

async def sequential():
    start = time.perf_counter()
    a = await fetch("a", 1)
    b = await fetch("b", 1)
    c = await fetch("c", 1)
    print(f"sequential: {time.perf_counter() - start:.2f}s")

async def concurrent():
    start = time.perf_counter()
    results = await asyncio.gather(
        fetch("a", 1),
        fetch("b", 1),
        fetch("c", 1),
    )
    print(f"concurrent: {time.perf_counter() - start:.2f}s")

asyncio.run(sequential())     # ~3 seconds
asyncio.run(concurrent())     # ~1 second — they ran in parallel
create_task — fire and remember·python
import asyncio

async def background_work():
    await asyncio.sleep(0.5)
    print("background done")

async def main():
    task = asyncio.create_task(background_work())
    print("task scheduled")
    # Do other work while it runs
    await asyncio.sleep(0.1)
    print("main is doing other things")
    await task                              # wait for the background to finish
    print("all done")

asyncio.run(main())
TaskGroup — modern structured concurrency (3.11+)·python
import asyncio

async def fetch(name, delay):
    await asyncio.sleep(delay)
    return f"{name}-result"

async def main():
    async with asyncio.TaskGroup() as tg:
        a = tg.create_task(fetch("a", 0.5))
        b = tg.create_task(fetch("b", 0.3))
        c = tg.create_task(fetch("c", 0.4))
    # All tasks complete by here, exceptions raised as ExceptionGroup
    print(a.result(), b.result(), c.result())

asyncio.run(main())
# Cleaner than gather + try/except for exception handling
Forgetting await — the most common bug·python
import asyncio

async def add(a, b):
    return a + b

async def main():
    # WRONG — no await, you get the coroutine, not the result
    result = add(2, 3)
    print(type(result))                 # <class 'coroutine'>
    print(result)                       # <coroutine object ...>

    # RIGHT
    result = await add(2, 3)
    print(result)                       # 5

asyncio.run(main())

# Python warns about un-awaited coroutines on shutdown

External links

Exercise

Write async def fetch(url: str, delay: float) -> str that awaits a sleep(delay) then returns f"got {url}". Then write async def main() that runs three concurrent fetches with different delays using asyncio.gather. Time it with time.perf_counter. Then rewrite using asyncio.TaskGroup. Verify both versions produce the same output and same elapsed time.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.