Why are contextvars better than threading.local in async Python?

This tests whether you know async tasks share OS threads, making thread-local storage unsafe for request state. A great answer notes ContextVar is task-local and resets automatically, while threading.local bleeds across concurrent coroutines.
WHAT THIS TESTS: The interviewer wants to know if you grasp the execution model of async Python. In an async framework like FastAPI, a single OS thread runs an event loop that interleaves thousands of coroutines. State tied to the thread via threading.local is therefore visible to every concurrent request on that thread, not just the one that set it. This question checks whether you understand that contextvars provides task-local or context-local isolation that matches the logical flow of async/await code rather than the underlying thread.
A GOOD ANSWER COVERS: A good answer hits four things in order. First, explain the failure mode of threading.local in async code: because multiple requests run interleaved on the same event-loop thread, a thread-local variable set by request A is visible to request B before A has finished. Second, introduce ContextVar as the PEP 567 solution added in Python 3.7. Third, describe how asyncio propagates context: when a new Task is created, the current Context is copied, so a request ID set at the entry point travels with that task and its child tasks without affecting other concurrent work. Fourth, mention the Token mechanism: ContextVar.set returns a token that can be used with reset or as a context manager to restore the previous value, preventing state leakage when libraries or middleware re-enter. Optionally note that copy_context has O(1) complexity, so reading or copying contexts is cheap even with many variables.
COMMON WRONG ANSWERS: Red flags include suggesting that locks around threading.local fix the problem; locks serialize access but do not isolate data, so request B still sees request A's ID once the lock releases. Another red flag is claiming that contextvars are just global variables; they are global identifiers but their values are scoped to the execution context. Confusing contextvars with coroutine-local storage is also weak; contextvars follow the logical context across await boundaries and task boundaries, not just a single coroutine object.
LIKELY FOLLOW-UPS: The interviewer may ask how you would implement request ID logging with contextvars in FastAPI middleware. They might ask how contextvars interact with thread pools, for example when using run_in_executor; in that case you must manually propagate the context into the worker thread because the executor runs on a different OS thread. They could also ask about testing: how to use copy_context to capture and assert on context state in unit tests.
ONE CONCRETE EXAMPLE: Imagine a FastAPI app with middleware that sets request_id = ContextVar('request_id'). In the endpoint, you call an async helper that logs the ID. With threading.local, if two requests hit the same worker thread, the second middleware might overwrite request_id before the first request's helper resumes after an await, logging the wrong ID. With ContextVar, each Task gets its own copied context; the first request's helper resumes and still sees its own request_id because the context snapshot travels with the Task.
Source: docs.python.org
Read the original → docs.python.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.