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

    Programming code representing operations as functions with Python operator
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python operator: Operations as Functions

    Learn Python operator to use operations as functions, sort fields, access items, call methods, and build clear functional pipelines.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Three-dimensional alphabet representing Unicode normalization with Python unicodedata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python unicodedata: Normalize Unicode

    Learn Python unicodedata to normalize Unicode, inspect names, categories, numeric values, combining marks, bidirectional classes, and width.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Server network representing resource management with Python ExitStack
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExitStack: Manage Resources

    Learn Python ExitStack to manage dynamic files, connections, callbacks, and cleanup safely in predictable reverse order.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Locked folder representing file types and permissions with Python stat
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stat: File Types and Permissions

    Learn Python stat to interpret file types, permissions, links, timestamps, Windows attributes, and Unix flags safely and portably.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Laptop with code representing automatic documentation with Python pydoc
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pydoc: Automatic Documentation

    Learn Python pydoc to generate terminal help, HTML, search, and a local documentation server safely from docstrings.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026
    International keyboard representing numbers, currency, and dates with Python locale
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python locale: Numbers, Currency, and Dates

    Learn Python locale to format and parse numbers, currency, dates, encodings, and cultural sorting without concurrency mistakes.

    Ler mais

    Tempo de leitura: 6 minutos
    09/08/2026