The contextvars module stores information associated with the current execution context. Unlike a global variable, a ContextVar can hold different values in asynchronous tasks, threads, and copied contexts. It is useful for request IDs, tracing, the current user, locale, transactions, and observability data that should follow a call chain without being passed through every function signature.
Context variables should not replace explicit parameters for business data. They work best for controlled cross-cutting context. Excessive hidden state makes code and tests harder to understand. Define who sets a variable, when it is restored, and how far it may propagate.
Create a ContextVar
Declare it at module level.
from contextvars import ContextVar
request_id = ContextVar("request_id")
The name appears in diagnostics and representations. Choose a clear stable identifier.
Set and read a value
set() associates a value with the current context and get() retrieves it.
request_id.set("req-123")
print(request_id.get())
The value is not a process-wide shared global; each context can have a separate association.
Defaults
A default can be defined in the constructor.
current_locale = ContextVar("current_locale", default="en-US")
Use a default only when absence is a valid state. Leaving required context without a default makes missing setup easier to detect.
LookupError
Calling get() without a value or default raises LookupError.
try:
identifier = request_id.get()
except LookupError:
identifier = "no-context"
Do not silently hide the error when the context is supposed to be mandatory.
Tokens
set() returns a Token representing the previous state.
token = request_id.set("req-456")
try:
execute()
finally:
request_id.reset(token)
The try/finally pattern prevents values from leaking into later work in the same context.
Restore instead of clearing
reset(token) restores the previous state, which may be another value or no value at all.
Assigning None is not equivalent and may overwrite an outer context that should return after the block.
Build a context manager
A wrapper makes set/reset reusable.
from contextlib import contextmanager
@contextmanager
def use_request_id(value):
token = request_id.set(value)
try:
yield
finally:
request_id.reset(token)
See Python contextlib for synchronous and asynchronous managers.
Integration with asyncio
Tasks normally receive the appropriate copy of the current context when they are created.
import asyncio
async def worker(name):
print(name, request_id.get())
async def main():
token = request_id.set("req-main")
try:
await asyncio.gather(worker("a"), worker("b"))
finally:
request_id.reset(token)
Each task can change its own value without overwriting the others.
Task creation time
The moment a task is created influences the context it captures.
Set values before creating a task when inheritance is required. Use explicit context options provided by the target Python version when tighter control is needed.
Do not use threading.local for async tasks
threading.local() separates data by thread, but many asynchronous tasks share one thread.
ContextVar was designed to preserve logical isolation among those tasks.
Threads
Each thread has its own context stack. Values do not automatically appear in a new thread.
Copy the context explicitly or pass required data as arguments.
copy_context
copy_context() creates a shallow copy of the current context.
from contextvars import copy_context
context = copy_context()
context.run(function)
The variable associations are copied, but mutable objects stored as values are still the same underlying objects.
Propagate to an executor
Capture a context before submitting work to a thread.
context = copy_context()
future = executor.submit(context.run, process, item)
Do not enter the same Context concurrently in several threads. Create one copy per submission when necessary.
Mutable values
Putting a dictionary or list in a ContextVar does not make it immutable or isolated.
Prefer immutable values or copy before mutation. Otherwise contexts may share internal state.
Request IDs
Middleware can set an ID on entry and restore it on exit.
def handle(request):
token = request_id.set(request.id)
try:
return process(request)
finally:
request_id.reset(token)
The outer association is restored even after an exception.
Logging
Logging filters or adapters can read the current value and add it to records.
class ContextFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get("-")
return True
Do not place passwords, tokens, or unnecessary personal information in logging context.
Tracing
Trace IDs and span IDs are common examples of cross-cutting context.
Observability libraries may already use context variables internally. Integrate through their public API to avoid competing sources of truth.
Locale and timezone
An application may store locale or timezone preference for one request.
Validate and reset the value. Pure functions that receive locale explicitly remain easier to test.
Transactions
A transaction identifier or session may be contextual, but connections and commits have critical lifecycles.
Do not hide commit, rollback, or closure. Combine context variables with explicit context managers.
Callbacks
The context in which a callback executes depends on when and how it was registered.
Do not assume propagation through third-party libraries. Test it or capture context explicitly.
Background tasks
A background job should not automatically inherit every detail from a request, especially credentials and large objects.
Create a clean context or copy only the fields required by the job.
Context.run
Context.run(callable, ...) enters a context, calls the function, and restores the previous thread context afterward.
result = context.run(function, argument)
Exceptions propagate normally.
Inspect a context
A Context acts like a mapping for diagnostics.
for variable, value in copy_context().items():
print(variable.name, value)
Classify sensitivity before logging values.
Performance
Context operations are efficient for normal cross-cutting use, but they should not replace local variables inside hot inner loops.
Measure before adding many lookups to performance-critical paths.
Public APIs
A library may use context variables internally, but should document how context is established and restored.
Do not require users to manipulate private tokens or depend on internal variable names.
Testing
Every test should establish the context it needs and restore it afterward.
token = request_id.set("test")
try:
assert execute() == expected
finally:
request_id.reset(token)
Run concurrent tasks with different values to detect leaks.
Test isolation
Fixtures that forget to reset context can contaminate later tests running in the same thread.
Use context managers or fixtures with guaranteed cleanup.
Security
Implicit context may carry identity or authorization. Never trust a contextual value without validating the operation itself.
Reduce propagation to background jobs and do not expose the complete context in public error messages.
Common mistakes
Common failures include using globals or threading.local in asyncio, forgetting reset(), assigning None instead of restoring a token, storing mutable objects, assuming propagation to threads, and hiding business data in context.
Conclusion
contextvars provides execution-local context for threads and asynchronous tasks. Use ContextVar for cross-cutting data, tokens for restoration, and copy_context() for explicit propagation.
Keep values small, avoid secrets, test isolation, and continue using explicit parameters for business rules. Consult the official contextvars documentation and Python contextlib.







