contextvars.Token represents the change produced when a value is assigned to a ContextVar. Starting with Python 3.14, the token can also be used as a context manager, so a temporary value can be installed with with and the previous state is restored automatically when the block ends. This is useful in asynchronous services, middleware, structured logging, request tracing, testing, localization, and other systems where contextual data must follow an execution without relying on unsafe global variables.
Context variables are designed to keep values isolated between concurrent tasks. In an asyncio program, multiple coroutines may run on the same thread while seeing different values for the same ContextVar. The token records enough information to undo a specific change, whether the variable had an earlier value or had never been set in that context.
Why globals are unsafe
A module-level global may look convenient for storing a request ID, current user, tenant, or trace code. Under concurrency, however, one request can overwrite another request’s value. Logs become mixed, authorization checks can use the wrong identity, and failures are hard to reproduce. Thread-local storage is not a complete solution for asynchronous servers because many tasks can share one operating-system thread.
ContextVar associates a value with the current execution context. Python propagates that context when tasks are created, allowing infrastructure code to read contextual information without every function receiving another parameter. This is especially valuable for web frameworks, queue consumers, observability libraries, database instrumentation, and background jobs.
Traditional set and reset
from contextvars import ContextVar
request_id = ContextVar("request_id", default="no-id")
def process():
token = request_id.set("req-123")
try:
print(request_id.get())
finally:
request_id.reset(token)
process()
print(request_id.get())
The set() method returns a token. That token belongs to the exact variable and exact change that produced it. Calling reset(token) restores the earlier state. The finally block is essential because exceptions, cancellation, and early returns must not leak the temporary value into later work.
Using Token as a context manager
In Python 3.14 and newer, the token returned by set() supports the context-manager protocol. The code becomes shorter and its lifetime is visible directly in the indentation.
from contextvars import ContextVar
current_user = ContextVar("current_user", default="anonymous")
with current_user.set("alex"):
print(current_user.get())
print(current_user.get())
Inside the block the value is alex. After the block, the previous value is restored automatically. This form is a good fit for short scopes, test setup, middleware, temporary feature flags, and library adapters.
Nested values
Context changes can be nested. Each token restores the state that existed immediately before its own set().
level = ContextVar("level", default="info")
with level.set("debug"):
print(level.get())
with level.set("trace"):
print(level.get())
print(level.get())
print(level.get())
The values are debug, trace, debug, and info. This predictable stack-like behavior prevents outer layers from accidentally restoring the wrong value after inner components have temporarily changed the same variable.
Isolation with asyncio
import asyncio
from contextvars import ContextVar
correlation_id = ContextVar("correlation_id", default="-")
async def worker(name):
with correlation_id.set(f"job-{name}"):
await asyncio.sleep(0.01)
print(name, correlation_id.get())
async def main():
await asyncio.gather(worker("A"), worker("B"))
asyncio.run(main())
Even though the tasks are interleaved, each worker observes its own correlation ID. No global locking is required, and the value does not have to be threaded through every helper function. Still, context variables should not replace all explicit parameters. Core business data is often clearer and easier to test when it remains part of a function’s interface.
Structured logging
A common pattern is to inject a request identifier into every log record.
import logging
from contextvars import ContextVar
request_id = ContextVar("request_id", default="-")
class ContextFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get()
return True
handler = logging.StreamHandler()
handler.addFilter(ContextFilter())
handler.setFormatter(logging.Formatter(
"%(request_id)s %(levelname)s %(message)s"
))
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
A middleware layer can set the request ID once, and all code executed inside that scope receives the correct value. When the request finishes, automatic restoration prevents the next request handled by the worker from inheriting stale context.
Middleware and exceptions
async def middleware(handler, request):
identifier = request.headers.get("X-Request-ID", "generated-locally")
with request_id.set(identifier):
return await handler(request)
The context manager restores the previous state even if the handler raises an exception or the task is cancelled. That matters in production servers, where cancellation, timeouts, validation errors, and early responses are normal control-flow events.
Token safety rules
A token must be reset only through the variable that created it. It also cannot be reused after it has already restored the state. Python rejects invalid operations instead of silently corrupting the context. Keep a token only as long as needed and prefer a with block when the scope is local.
Do not move tokens between tasks. A child task may inherit context values, but a token describes a concrete mutation in the context where it was created. Treat it as a short-lived restoration handle, not as general application data.
Token.MISSING
If the variable had no previous value, token.old_value can be Token.MISSING. This differs from an explicit previous value of None.
from contextvars import ContextVar, Token
key = ContextVar("key")
token = key.set(None)
if token.old_value is Token.MISSING:
print("there was no previous value")
key.reset(token)
The distinction allows Python to restore an unset state accurately instead of replacing it with a normal value.
Supporting older Python versions
The token context-manager syntax requires Python 3.14 or later. Libraries that support earlier versions should keep the explicit set(), try, finally, and reset() pattern, or provide a compatibility helper.
from contextlib import contextmanager
@contextmanager
def temporary_value(var, value):
token = var.set(value)
try:
yield
finally:
var.reset(token)
This helper offers nearly the same call-site experience and can be removed after the project’s minimum version moves to Python 3.14.
Testing contextual behavior
Temporary contexts make tests independent and expressive. A test can install a user, locale, tenant, or trace identifier without leaving persistent state behind.
def test_temporary_context():
language = ContextVar("language", default="en")
with language.set("es"):
assert language.get() == "es"
assert language.get() == "en"
Also test exception paths to verify restoration. For asynchronous code, start concurrent tasks with different values and assert that no task observes another task’s context. Avoid tests that depend on execution order unless ordering is part of the actual requirement.
Mutable objects
A context variable isolates the reference stored in the context; it does not make the referenced object immutable. If several contexts point to the same dictionary or list, mutations to that object may still be visible across tasks. Prefer immutable values, small data classes, tuples, identifiers, or newly created objects when isolation matters.
Performance and architecture
ContextVar is designed for frequent access, but it still introduces indirection. Use it for cross-cutting information such as tracing, security context, localization, and telemetry. Do not hide essential domain dependencies in context simply to shorten function signatures.
Libraries should document which context variables they read, who is expected to set them, and what happens when they are missing. Safe defaults are valuable, but silently inventing security-sensitive values can be dangerous. For authentication and authorization, fail explicitly when required context is unavailable.
Copying and running contexts
The contextvars module also provides copy_context(), which captures the current context. Advanced frameworks can run a callable inside a selected context, but most application code should rely on normal task propagation. Manual context copying is useful when bridging custom executors or callback systems, yet it should be covered by concurrency tests because ownership becomes less obvious.
Best practices
Define context variables at module scope, use descriptive names, choose safe defaults, keep changes narrowly scoped, and always restore state. Prefer with var.set(value) on Python 3.14 and explicit try/finally on older versions. Store small immutable values whenever possible. Avoid tokens that outlive the operation that created them.
For related topics, read the Academify guides on asynchronous programming with asyncio, logging in Python, context managers, and threading in Python. External references include the official contextvars documentation and the official with-statement reference.
Conclusion
Context-manager support for contextvars.Token makes temporary context changes clearer and safer. It removes manual cleanup boilerplate, restores state through exceptions, and preserves task isolation in asynchronous programs. Used carefully, it improves request tracing, middleware, testing, localization, and observability. The key architectural rule is to reserve context for cross-cutting execution metadata while keeping important business dependencies explicit.







