Python contextvars: Async Context

Published on: August 28, 2026
Reading time: 5 minutes
Vivid close-up of a green tree python coiled on a branch in the rainforest.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A minimalist June calendar page adorned with red hearts for a romantic touch.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python sched: Schedule Events

    Learn Python sched to schedule events, use priorities, cancel work, create drift-free recurrence, test with fake clocks, and integrate executors.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Stack of colorful sticky notes with a 'To Do' note on top, isolated on white.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python bisect: Keep Lists Sorted

    Learn Python bisect for binary search, ordered insertion, duplicates, key functions, ranges, rankings, and safe concurrent updates.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of a vibrant yellow python coiled with textured scales in vibrant light.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python heapq: Priority Queues

    Learn Python heapq for priority queues, top-k selection, sorted merges, stable ties, lazy deletion, updates, and backpressure.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Detailed shot of a yellow measuring tape with numbers on a dark background.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python array: Compact Numeric Data

    Learn Python array for compact numeric storage, typecodes, bytes, files, memoryview, portable binary layouts, and safe validation.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python struct: Binary Data Formats

    Learn Python struct for binary packing, endianness, offsets, padding, buffers, sockets, versioned layouts, and safe validation.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Composition of black and white photos and old travel log placed with vintage island map and obsolete photo camera on narrow wooden table
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python mmap: Memory-Mapped Files

    Learn Python mmap for memory-mapped files, byte searches, in-place edits, shared pages, aligned offsets, and safe synchronization.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026