TRT
Sign in
easyCoreconcept

Why does `threading.local()` break in `asyncio`, how does `contextvars` use a persistent HAMT to achieve O(1) context copying across `create_task()`, and what.

Contrast OS-thread-bound storage with `Task._step()` context swapping, explain HAMT structural sharing, and show `token = var.set()` / `var.reset(token)`.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Because all coroutines on an asyncio event loop share the same single OS thread, threading.local() is global across all concurrent requests on that worker! Python's contextvars module (PEP 567) solves this by binding state to the logical execution context (contextvars.Context, backed by a persistent HAMT—Hash Array Mapped Trie) rather than the OS thread. Every asyncio.Task captures a $O(1)$ shallow copy-on-write snapshot of the current Context when create_task() is called, and Task._step() swaps that task's Context active before stepping the coroutine and restores the previous context on await! Setting var.set(val) returns a Token so middleware can restore the exact prior value via var.reset(token) in a finally block.

Each asyncio.Task executes inside its own isolated contextvars.Context copy: mutating a ContextVar inside Task A is visible to all synchronous functions and child coroutines awaited by Task A, but is completely invisible to sibling Task B running concurrently on the same OS thread. Because contextvars.Context uses an immutable Hash Array Mapped Trie (HAMT), copy_context() at create_task() time is an $O(1)$ pointer copy, and var.set() inside a task allocates a new HAMT path only for that task's context pointer without mutating any parent or sibling task's HAMT.

Reading request_id_var.get() takes ~25 ns (cached pointer lookup), and copying the entire Context when spawning a new asyncio.Task is $O(1)$ regardless of how many ContextVars exist. In production systems (OpenTelemetry Python trace context propagation (trace.get_current_span()), structlog contextvars, Starlette/FastAPI request context middleware, and SQLAlchemy async session scoping all run on contextvars.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how contextvars.ContextVar copy-on-write context propagation operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (ContextVar.get(), copy_context() on create_task()) with concrete numbers.
  • Identifies real-world failure modes (Mutating a list/dict returned by var.get() in place (var.get().append(x))) and explains how production systems mitigate them.

Common mistakes

  • Treating contextvars.ContextVar copy-on-write context propagation as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Using a raw ThreadPoolExecutor.submit(fn) without contextvars.copy_context().run).
  • Forgetting boundary conditions and error recovery (Forgetting var.reset(token) in middleware when a task pool reuses contexts).

This answer walks

Practice follow-ups

  1. 01What happens if you define `tags_var = ContextVar('tags', default=[])` and a request runs `tags_var.get().append('admin')` instead of `tags_var.set(...)`?
  2. 02When a child task spawned via `asyncio.create_task()` calls `var.set('new')`, does the parent task see `'new'`? Why or why not?

Not started

Sign in to save your learning progress.

Sign in to save