Video summary

import asyncio: Learn Python's AsyncIO #4 - Coroutines Under The Hood

Main summary

Key takeaways

Educational

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(...)
      • consumer checks/awaits:
        • whether it’s done or cancelled
        • and retrieves the value (or gets an error)
    • 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 def can await a future and proceed with the returned value.
    • If the future completes with an exception, the awaiting coroutine’s try/except can handle it.
  • 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 to KeyboardInterrupt), so broad except 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.
  • Historical perspective: Python 3.4 to modern Python (3.8/2020)

    • Python 3.4 / early async design
      • @asyncio.coroutine existed and coroutines were based on generators (yield from, not async/await keywords initially).
      • Coroutines were “marked generators” (decorator adds attributes/handles special cases).
      • yield from and 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.
  • How “steps” map to await boundaries (important conceptual insight)

    • A task step runs until it hits an actual await boundary.
    • 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).
  • 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 async code.
  • 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)

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.
    • Optionally cancel:
      • call cancellation on the future (cancellation propagates differently than normal exceptions).
  • 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 future inside async def to integrate into async flow.

Awaiting and composing futures/coroutines

  • Write an async def function that:
    • uses await on a future or awaitable
    • optionally wraps await in try/except to handle exceptions from the awaited object
  • 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 await hits an actual async boundary
    • then the task schedules another step back onto the event loop (trampoline/fair scheduling)
  • 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 from and generator delegation details mentioned)
  • Library/module names referenced
    • asyncio, tasks (including tasks.py)
    • internal implementation modules like _asyncio / _asyncio-related code (“underscore async i/o” mentioned)
    • asyncio.Future / asyncio.tasks.Task concepts and event loop scheduling functions (e.g., “call soon”/“call later” mentioned)

Original video