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's really being asked
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.
The full answer
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.
The mistakes people make
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.
What usually comes next
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.
A 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.
Interview question
When using run_in_executor in asyncio, why must contextvars be manually propagated into the worker thread?
- a.Thread pool workers run in separate processes, so they need the context serialized manually
- b.asyncio.copy_context has O(1) complexity only when the context is passed manually into the thread
- c.The executor runs on a different OS thread, so the event loop's task context is not automatically present thereCorrect
- d.ContextVar values are scoped to individual coroutine objects and do not survive crossing into sync functions
Why? this is the answer
The executor runs on a different OS thread, so the event loop's task context is not automatically present there. The coroutine-local distractor is wrong because contextvars propagate across await and task boundaries, not just a single coroutine object.
Just read this? Test yourself on what you have been reading.
Read the original → docs.python.org
- #python
- #asyncio
- #contextvars
- #fastapi
- #concurrency
You just looked this up. Could you explain it out loud?
That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on python — each one lists the topics its interview covers.
See open roles