CHAPTER 44 · ADVANCED4 min read

Async, Await, and Tasks

Understand cooperative scheduling and structure concurrent async operations with explicit task ownership.

Python 3.12+Free to readModule 10 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Distinguish a coroutine object from a scheduled task.
  • Use TaskGroup for related concurrent operations.
  • Keep blocking work off the event loop.

01Await is a scheduling boundary

Calling async def creates a coroutine object; it does not run the body to completion. An event loop drives coroutines, allowing another task to run when the current task suspends at an await that actually waits. This is cooperative scheduling. Ordinary Python instructions between suspension points still execute synchronously in the event-loop thread.

await does not mean parallel execution. Awaiting one operation and then another usually performs them sequentially. Scheduling both as tasks allows their waits to overlap, but their Python CPU work still competes for the same event-loop thread. Async is useful when libraries expose asynchronous I/O and many operations spend time waiting; it is not a shortcut for accelerating a CPU-heavy loop.

sequential_async.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02Own tasks with a structured scope

A task schedules a coroutine for execution and provides a handle to its eventual result. TaskGroup gives related tasks a shared lifetime: leaving the context waits for them, and a non-cancellation failure triggers cancellation of the remaining tasks. This makes it easier to see who owns background work and where its failures will be observed.

Create all independent operations before awaiting the group. Store task handles if you need results in a stable input order; completion order is not guaranteed. When several tasks fail, TaskGroup can raise an ExceptionGroup. Handle the group deliberately when recovery is required instead of assuming every concurrent failure arrives as one ordinary exception.

task_group.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Blocking code blocks every task

time.sleep, synchronous urlopen, and a long calculation inside async def still block the event loop. Naming a function async does not transform its operations. Use async-native I/O libraries where appropriate. asyncio.to_thread can move blocking I/O into a worker thread, but the original operation may keep running even if the coroutine awaiting it is cancelled.

For CPU-heavy Python work on conventional CPython, consider a process pool rather than to_thread. Keep event-loop callbacks short and measure responsiveness under realistic load. A progress spinner that updates in a toy example is insufficient evidence that the application stays responsive while parsing a huge response or serializing a large report.

thread_bridge.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Choose one application entry point

asyncio.run creates an event loop, runs the top-level coroutine, and performs shutdown. Call it at the boundary of a command-line program, rather than repeatedly inside library functions. Environments with an already running loop, such as some notebooks, usually require awaiting the coroutine directly. Library APIs should communicate whether they are synchronous or asynchronous.

Avoid fire-and-forget tasks whose handles are lost. Keep strong references and arrange shutdown if a task must outlive one request. Define whether failure in one task invalidates the whole operation or merely one result. TaskGroup is a strong default for related work, while a deliberate result-collection policy is needed when partial success is a product requirement.

LET IT CLICK

The ideas worth keeping.

  • Coroutine construction, task scheduling, and awaiting are different actions.
  • TaskGroup makes related task lifetime and failure ownership visible.
  • Async code must avoid blocking the event-loop thread.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Concurrent local lookups

Schedule three async lookups inside one TaskGroup. Return their doubled IDs in original input order.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
import asyncioasync def lookup(value):    await asyncio.sleep(0)    return value * 2async def collect(values):    async with asyncio.TaskGroup() as group:        tasks = [group.create_task(lookup(value)) for value in values]    return [task.result() for task in tasks]assert asyncio.run(collect([3, 1, 2])) == [6, 2, 4]

The tasks exist together inside one scope. Reading handles after the context exits preserves input order and observes completed results.

02

Keep a blocking adapter explicit

Wrap a synchronous fixture reader using to_thread and assert its result. Explain why cancellation would not forcibly stop that reader.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
import asynciodef read_fixture():    return {'count': 4}async def load():    return await asyncio.to_thread(read_fixture)assert asyncio.run(load()) == {'count': 4}

The adapter makes the boundary visible. A cancellation request stops the awaiter cooperatively; it does not provide a general mechanism to kill work already executing in a thread.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What does calling an async function return before scheduling?
02Two awaits written one after another guarantee which behavior?
03What happens if time.sleep runs in the event-loop thread?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Concurrent local lookups

Schedule three async lookups inside one TaskGroup. Return their doubled IDs in original input order.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.