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.ContextVarcopy-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()oncreate_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.ContextVarcopy-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)withoutcontextvars.copy_context().run). - Forgetting boundary conditions and error recovery (Forgetting
var.reset(token)in middleware when a task pool reuses contexts).