Python contextvars: Async Context State

Published on: August 11, 2026
Reading time: 4 minutes
Network data flow representing asynchronous context with Python contextvars

The contextvars module stores values local to an execution context. It solves a common concurrency problem: making request IDs, users, tenants, locale, or tracing metadata available across layers without passing every value through every function and without leaking state from one task into another.

Thread-local storage can work in synchronous threaded code. In asyncio, many tasks share one thread and interleave execution, so ContextVar follows the logical task instead.

Declare a ContextVar

Create context variables at module scope, not inside closures. Context objects hold strong references to variables, so dynamic creation may prevent garbage collection.

from contextvars import ContextVar

request_id: ContextVar[str] = ContextVar("request_id")
current_user: ContextVar[str | None] = ContextVar(
    "current_user", default=None
)

The name is used for introspection and debugging. The optional default should match the expected type.

Read the current value

get() looks in the current context. It returns the method argument default first, then the variable’s declared default, or raises LookupError.

print(current_user.get())

try:
    print(request_id.get())
except LookupError:
    print("request_id is not set")

For required metadata, omitting a default can expose integration errors early. For genuinely optional data, a clear default simplifies callers.

Set and restore with a token

set() changes the value in the current context and returns a Token that restores the exact previous state.

token = request_id.set("req-123")
try:
    process()
finally:
    request_id.reset(token)

The same token cannot be used twice and belongs to the variable that created it.

Tokens as context managers in Python 3.14

Python 3.14 allows tokens returned by set() to be used as context managers.

with request_id.set("req-456"):
    print(request_id.get())

# previous value restored

For compatibility with older versions, use set(), try/finally, and reset().

Isolation in asyncio

When a task is created, its current context is copied. Later changes inside one task remain isolated.

import asyncio
from contextvars import ContextVar

name = ContextVar("name")

async def work(value):
    with name.set(value):
        await asyncio.sleep(0.01)
        return name.get()

async def main():
    result = await asyncio.gather(work("A"), work("B"))
    print(result)

asyncio.run(main())

Even after suspension at await, each task reads its own value.

A ContextVar is not an ordinary global

The variable object is global, but the associated value depends on the current context. Mirroring the value in a normal global destroys isolation.

Do not use context to hide domain data that should be explicit arguments. It works best for small cross-cutting metadata.

Request IDs in logs

request_id = ContextVar("request_id", default="-")

def log(message):
    print(f"[{request_id.get()}] {message}")

async def handle_request(identifier):
    with request_id.set(identifier):
        log("start")
        await call_service()
        log("finish")

Logging frameworks can use filters or adapters that read the variable. Avoid adding secrets or unnecessary personal data.

Tenant and user state

A multitenant application may expose the current tenant through context, but every database query must still enforce filters and authorization. Context propagation is not a security boundary.

Set the value at the request boundary, validate the principal, and restore it at the end.

copy_context

copy_context() copies the current context in O(1) time, regardless of the number of variables.

from contextvars import copy_context

ctx = copy_context()
for variable, value in ctx.items():
    print(variable.name, value)

The copy can execute code with ctx.run(function, *args). Changes stay in that Context object rather than the outside context.

Run code in a specific context

ctx = copy_context()

def task():
    request_id.set("isolated")
    return request_id.get()

result = ctx.run(task)

The same context cannot be entered concurrently more than once, even from another thread. Doing so raises RuntimeError. After exit, it can be entered again.

Propagation to threads

Each thread has its own effective context stack. Executor propagation depends on the API. For explicit behavior, capture a context and run the function inside it.

ctx = copy_context()
future = executor.submit(ctx.run, function)

Do not submit the same context concurrently to multiple workers. Create separate copies.

ContextVar versus threading.local

threading.local() isolates values per physical thread. Hundreds of asyncio tasks may share one thread, so they would see the same thread-local state. ContextVar tracks the logical context and integrates with asyncio.

Mutable defaults

Avoid shared mutable defaults such as lists and dictionaries. Context isolation applies to the reference, not to mutations of the same object.

# avoid
errors = ContextVar("errors", default=[])

# create a value for each scope
with errors.set([]):
    errors.get().append("failure")

Immutable values, IDs, and frozen dataclasses reduce surprises.

Nested tokens and reset order

Sets can be nested. Restore them in reverse order.

t1 = request_id.set("outer")
t2 = request_id.set("inner")
request_id.reset(t2)
request_id.reset(t1)

Using the wrong token or reusing it raises an error. Python 3.14 context-manager syntax makes nesting more visible.

Callbacks and task scheduling

Callbacks usually execute in the context captured by the scheduling API, but frameworks may define their own rules. Test propagation with the real event loop, task factory, executors, and callbacks used by the application.

Testing isolation

async def test_isolation():
    async def read(value):
        with request_id.set(value):
            await asyncio.sleep(0)
            return request_id.get()

    a, b = await asyncio.gather(read("A"), read("B"))
    assert (a, b) == ("A", "B")

Also test exceptions, cancellation, subtasks, and thread execution. Confirm that the previous value is restored after every test.

Do not build a hidden dependency container

Storing database clients, HTTP clients, and full services in context hides dependencies and complicates tests. Pass primary dependencies explicitly. Reserve context variables for small contextual metadata.

Common mistakes

  • Creating ContextVars inside closures.
  • Forgetting to reset a token.
  • Using a shared mutable default.
  • Treating context as authorization.
  • Expecting thread-local storage to isolate asyncio tasks.
  • Entering the same Context concurrently.
  • Hiding major dependencies in context state.
  • Declare ContextVars at module scope.
  • Use meaningful names.
  • Restore values with tokens or context managers.
  • Prefer small immutable values.
  • Capture context explicitly across threads.
  • Test cancellation and concurrency.
  • Keep authorization outside contextual metadata.

Continue with Python ExitStack, Python inspect, Python faulthandler, Python traceback, and Python operator.

See the official contextvars documentation and PEP 567.

Conclusion

contextvars provides context-local state for synchronous and asynchronous code. It is ideal for request IDs, tracing, locale, and small cross-cutting metadata. Safe use requires clear scopes, guaranteed restoration, immutable defaults, and tests across tasks and threads.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026