Video summary
import asyncio: Learn Python's AsyncIO #4 - Coroutines Under The Hood
Main summary
Key takeaways
Main ideas and lessons
-
AsyncIO’s core building blocks
- Futures: standardized “placeholder” objects that will eventually hold either a result or an exception.
- Coroutines: functions that run cooperatively by yielding control at
await/ IO boundaries. - Tasks: wrappers around coroutines that schedule them on the event loop and drive them forward step-by-step.
-
Futures as a communication + control mechanism
- A future represents a value “not available yet”.
- Both producer (code that computes the value) and consumer (code that awaits/reads it) use the same protocol:
- producer sets:
- result via
set_result(...), or - failure via
set_exception(...)
- result via
- consumer checks/awaits:
- whether it’s done or cancelled
- and retrieves the value (or gets an error)
- producer sets:
- Futures can also be cancelled, allowing consumers/producers to stop caring about the outcome.
-
Awaiting futures
- Futures can be awaited just like coroutines, enabling composition:
- an
async defcanawaita future and proceed with the returned value.
- an
- If the future completes with an exception, the awaiting coroutine’s
try/exceptcan handle it.
- Futures can be awaited just like coroutines, enabling composition:
-
Cancellation behavior and Python version detail
- Cancellation is not swallowed like normal exceptions if handled correctly.
- Since Python 3.8, cancellation-related exceptions are based on
BaseException(similar in spirit toKeyboardInterrupt), so broadexcept Exception:handling is less likely to incorrectly swallow cancellations. - The video warns against the “meme”/bad practice of catching “everything” (often referred to as catching all exceptions indiscriminately).
-
Concurrency with multiple awaits
- A single future (once completed) can be awaited by multiple coroutines.
- Multiple awaiting coroutines can be scheduled concurrently (e.g., via
asyncio.gather).
-
Callbacks on futures + event-loop scheduling
- Futures support done callbacks that run when the future is “done” (result set or error set).
- Key subtlety: callbacks don’t run immediately inside
set_result. - Instead, callbacks run when the event loop gets control (scheduled using mechanisms like
call_soon/call_later), which can surprise beginners.
-
Under-the-hood implementation principles
- Future internals
- The future stores the event loop reference (commonly passed in or captured via “current loop”).
- Setting a result schedules callbacks onto the event loop.
- Scheduling uses fair FIFO-style callback ordering to reduce starvation when many callbacks exist.
- Tasks drive coroutines using “steps”
- In older Python implementations (and conceptually), tasks execute coroutines by repeatedly:
- running a small chunk (“one step”) until the next await boundary,
- then rescheduling themselves back onto the event loop via callback/trampoline logic.
- This provides concurrency on a single thread by breaking execution into smaller pieces.
- In older Python implementations (and conceptually), tasks execute coroutines by repeatedly:
- Future internals
-
Historical perspective: Python 3.4 to modern Python (3.8/2020)
- Python 3.4 / early async design
@asyncio.coroutineexisted and coroutines were based on generators (yield from, notasync/awaitkeywords initially).- Coroutines were “marked generators” (decorator adds attributes/handles special cases).
yield fromand generator delegation explain how values/exceptions/control flow propagate.
- Modern native async
- Coroutines now have a distinct type (not just generator-marking).
- Tasks’ implementation moved largely into optimized/native code, but the conceptual model remains:
- task steps + rescheduling on the event loop.
- Python 3.4 / early async design
-
How “steps” map to await boundaries (important conceptual insight)
- A task step runs until it hits an actual
awaitboundary. - The video demonstrates step slicing with nested/recursive coroutines:
- inner coroutines may run “within” the same outer step until the next await boundary
- tasks do not necessarily create a new task for inner coroutines unless you explicitly spawn one
- A task acts as the “gateway” controlling execution of coroutine chains (delegation).
- A task step runs until it hits an actual
-
Big performance and correctness tradeoffs
- Async improves scalability for IO-bound workloads:
- network latency dwarfs the overhead of event loop + step slicing.
- this enables thousands of concurrent clients within one process.
- The downside:
- if a “step” runs too long (does blocking work or long CPU computation without yielding), it blocks the entire event loop.
- avoid blocking calls inside
asynccode.
- Async improves scalability for IO-bound workloads:
-
Top gotcha: forgetting
await- If you call an async function but don’t await it, Python warns and the code likely won’t do what you think.
- Another gotcha:
- if you return an un-awaited coroutine where a specific type is expected, type checking (with correct annotations) can catch the mismatch.
-
What’s next
- Next episode topics: async context managers, async iterators, async generators, plus library features like:
- servers, file descriptor watching, subprocess spawning
- synchronization primitives (including futures as one example)
- Next episode topics: async context managers, async iterators, async generators, plus library features like:
Methodology / instruction-like content (detailed bullet points)
Building and using a Future (producer/consumer protocol)
-
Producer side
- Create a
Future(often associated with an event loop). - When computation finishes:
- call
future.set_result(value)to publish the result, or - call
future.set_exception(exc)to publish failure.
- call
- Optionally cancel:
- call cancellation on the future (cancellation propagates differently than normal exceptions).
- Create a
-
Consumer side
- Check status:
future.done()(implied by “is it done?” style methods)future.cancelled()(implied by “is it cancelled?” style methods)
- Retrieve the outcome via the future’s result accessor:
- if the result isn’t ready, accessing it raises
- if the future failed, it raises the stored exception
- Prefer
await futureinsideasync defto integrate into async flow.
- Check status:
Awaiting and composing futures/coroutines
- Write an
async deffunction that:- uses
awaiton a future or awaitable - optionally wraps await in
try/exceptto handle exceptions from the awaited object
- uses
- For multiple concurrent waits:
- run multiple awaitables concurrently (e.g., using a gather-style utility)
- ensure the event loop is running until all work completes
Handling cancellation correctly
- When writing exception handling in async code:
- be cautious with broad exception catching patterns
- understand that cancellation exceptions should typically propagate rather than being swallowed
- rely on newer Python behavior where cancellation exceptions are not treated as normal “Exception” types
Using future callbacks
- If you register a “done callback” on a future:
- expect the callback to run when the event loop cycles again
- don’t assume it runs synchronously at the moment the future’s result is set
Understanding task stepping (what to design around)
- Assume a task executes coroutine logic in small chunks:
- each chunk runs until the next
awaithits an actual async boundary - then the task schedules another step back onto the event loop (trampoline/fair scheduling)
- each chunk runs until the next
- Design rule:
- do not run long blocking operations inside an async coroutine
- ensure your code yields at appropriate IO points
Common debugging gotchas
- Forgetting
await- If an async call is made but not awaited, it won’t execute as expected.
- Use Python warnings plus type checking to locate the mistake.
- Returning coroutine objects accidentally
- Type annotations can help detect returning the wrong thing (coroutine vs expected result type).
Speakers / sources featured
- Lucas (HDB) — narrator/host (“this is Lucas from HDB”).
- Python/PEPs referenced as sources (not spoken as individual people)
- PEP 3156 (initial async IO groundwork mentioned)
- PEP 380 (
yield fromand generator delegation details mentioned)
- Library/module names referenced
asyncio,tasks(includingtasks.py)- internal implementation modules like
_asyncio/_asyncio-related code (“underscore async i/o” mentioned) asyncio.Future/asyncio.tasks.Taskconcepts and event loop scheduling functions (e.g., “call soon”/“call later” mentioned)