# 50. Context Variables

# 50. Context Variables

Context variables provide task-local state for concurrent Python programs. They are implemented by the `contextvars` module and are designed mainly for asynchronous code, where many logical tasks can run on the same operating system thread.

A context variable looks like a global variable from the caller’s point of view, but its value belongs to the current logical execution context.

This solves a problem that thread-local storage cannot solve cleanly.

```python id="c5kq4u"
import contextvars

request_id = contextvars.ContextVar("request_id")

request_id.set("req-123")

print(request_id.get())
```

Output:

```text id="p3bq3k"
req-123
```

The value is not stored globally in the variable object. It is stored in the current context.

## 50.1 Why Context Variables Exist

Thread-local storage works when each request or unit of work has its own OS thread.

```python id="60c92p"
import threading

local = threading.local()

def handle_request(id):
    local.request_id = id
    process()
```

This breaks down for async code.

In async programs, many tasks share one thread:

```text id="5w9jxz"
one OS thread
    event loop
        task A
        task B
        task C
```

If all tasks share one thread, then thread-local state is shared by all tasks in that thread.

Context variables attach state to the logical context, not merely the OS thread.

## 50.2 The Core Problem

Suppose an async server wants each request to have a request ID.

A naive global variable fails:

```python id="x522b4"
current_request_id = None

async def handle_request(id):
    global current_request_id
    current_request_id = id
    await do_work()
    return current_request_id
```

If two requests interleave, one can overwrite the other’s value.

```text id="khlgkw"
task A sets request_id = A
task A awaits
task B sets request_id = B
task B awaits
task A resumes and sees B
```

Context variables prevent this by giving each task its own context value.

## 50.3 Basic `ContextVar`

Create a context variable:

```python id="j42uik"
from contextvars import ContextVar

request_id = ContextVar("request_id")
```

Set a value:

```python id="n9fmv9"
request_id.set("abc")
```

Get a value:

```python id="epub9c"
print(request_id.get())
```

If no value exists, `get()` raises `LookupError` unless a default is provided.

```python id="l6q17h"
request_id = ContextVar("request_id", default=None)

print(request_id.get())
```

Output:

```text id="iwgpcl"
None
```

## 50.4 ContextVar Objects Are Keys

A `ContextVar` object is a key into the current context.

```text id="veaqcs"
current context
    request_id variable -> "req-123"
    user_id variable    -> "user-9"
```

The variable object itself does not hold one global value.

This is why the same `ContextVar` can return different values in different tasks.

```python id="krcc7v"
request_id.get()
```

means:

```text id="dcd2zf"
look up request_id in the current context
```

## 50.5 Tokens

`set()` returns a token.

```python id="5w96u0"
token = request_id.set("req-123")
```

The token records the previous state for that variable in that context.

Use `reset()` to restore the previous value:

```python id="z9f0wb"
request_id.reset(token)
```

This is important for scoped changes.

```python id="qjewe4"
token = request_id.set("inner")
try:
    run_inner()
finally:
    request_id.reset(token)
```

Without reset, the value remains changed in the current context.

## 50.6 Scoped Context Values

A helper function can manage scoped values.

```python id="n6sde7"
from contextlib import contextmanager
from contextvars import ContextVar

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

@contextmanager
def bind_request_id(value):
    token = request_id.set(value)
    try:
        yield
    finally:
        request_id.reset(token)
```

Usage:

```python id="z24t6l"
with bind_request_id("req-1"):
    print(request_id.get())

print(request_id.get())
```

Inside the block, the value is set. After the block, the previous value is restored.

## 50.7 Async Task Isolation

Context variables are especially useful with `asyncio`.

```python id="3lo3et"
import asyncio
from contextvars import ContextVar

request_id = ContextVar("request_id")

async def inner():
    await asyncio.sleep(0)
    print(request_id.get())

async def handle(id):
    request_id.set(id)
    await inner()

async def main():
    await asyncio.gather(
        handle("A"),
        handle("B"),
    )

asyncio.run(main())
```

Expected output shape:

```text id="fqzejx"
A
B
```

Even though both tasks run on the same thread, each task keeps its own context value.

## 50.8 Context Capture on Task Creation

When an `asyncio` task is created, it captures the current context.

```python id="75pnye"
import asyncio
from contextvars import ContextVar

var = ContextVar("var", default="unset")

async def show():
    print(var.get())

async def main():
    var.set("before task")
    task = asyncio.create_task(show())

    var.set("after task")
    await task

asyncio.run(main())
```

The task sees:

```text id="2fz3h2"
before task
```

The context was copied when the task was created.

This behavior is central. Context variables follow logical task creation, not later global mutation.

## 50.9 Context Propagation

Context propagation means carrying context values across execution boundaries.

In async code, task creation usually captures context.

In threaded code, context is not automatically shared with new threads in the same way.

Example:

```python id="zaooq2"
from contextvars import ContextVar
import threading

var = ContextVar("var", default="unset")
var.set("main")

def worker():
    print(var.get())

threading.Thread(target=worker).start()
```

The worker thread may see:

```text id="tluu1f"
unset
```

because it has a separate context.

If you want propagation, copy the context explicitly.

## 50.10 `copy_context`

Use `copy_context()` to capture the current context.

```python id="mf8ka3"
from contextvars import copy_context

ctx = copy_context()
```

Run a callable inside that context:

```python id="fe5jbr"
ctx.run(function, arg1, arg2)
```

Thread propagation example:

```python id="yw76ti"
from contextvars import ContextVar, copy_context
import threading

var = ContextVar("var", default="unset")
var.set("main")

ctx = copy_context()

def worker():
    print(var.get())

thread = threading.Thread(target=ctx.run, args=(worker,))
thread.start()
thread.join()
```

Now the worker runs with the copied context.

## 50.11 Context Objects

A `Context` is a mapping-like object containing context variable bindings.

```python id="uxw91e"
from contextvars import ContextVar, copy_context

var = ContextVar("var")
var.set("value")

ctx = copy_context()

print(ctx)
print(ctx.get(var))
```

A context stores bindings from `ContextVar` objects to values.

You usually do not mutate a context directly. You run code inside it.

```python id="6iyjdj"
ctx.run(callable)
```

## 50.12 Contexts Are Immutable-Like Snapshots

A copied context behaves like a snapshot of the current bindings.

```python id="gp61bk"
var.set("A")
ctx = copy_context()
var.set("B")

ctx.run(lambda: print(var.get()))
print(var.get())
```

Output:

```text id="7v7vvr"
A
B
```

The copied context preserved `A`.

Running code inside a context can change that context’s values, but it does not mutate the caller’s current context.

## 50.13 Context Variables vs Thread Locals

| Feature | `contextvars` | `threading.local()` |
|---|---|---|
| Scope | Logical context | OS thread |
| Async task support | Good | Poor |
| Thread support | Explicit propagation | Natural per-thread storage |
| Main use | Request/task-local state | Thread-local caches and state |
| Propagation | Copyable context | Bound to thread |

Use `contextvars` for async request state.

Use `threading.local()` for state genuinely tied to an OS thread.

## 50.14 Context Variables vs Globals

A global variable has one binding per module.

```python id="bh0gl6"
current_user = None
```

A context variable has one binding per context.

```python id="xixfpb"
current_user = ContextVar("current_user")
```

Global variable problem:

```text id="50wjwl"
all tasks share one value
```

Context variable behavior:

```text id="x1l19i"
each logical context sees its own value
```

Context variables are still global objects, but their values are context-specific.

## 50.15 Request Context Example

```python id="9jgb6f"
from contextvars import ContextVar

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

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

async def handle_request(id):
    token = request_id.set(id)
    try:
        log("start")
        await do_work()
        log("end")
    finally:
        request_id.reset(token)
```

Every function called under `handle_request` can access the request ID without passing it explicitly.

This is useful for logging, tracing, metrics, and request-scoped configuration.

## 50.16 Tracing Example

Distributed tracing often needs an implicit current span.

```python id="2hz0ep"
current_span = ContextVar("current_span", default=None)

def start_span(name):
    parent = current_span.get()
    span = Span(name=name, parent=parent)
    token = current_span.set(span)
    return span, token

def end_span(token):
    current_span.reset(token)
```

Nested calls can create span trees without explicitly passing the parent span through every function.

This is one of the main practical uses of context variables.

## 50.17 Database Session Example

Some frameworks use context variables to store the current request or session.

```python id="9jtmr0"
current_session = ContextVar("current_session", default=None)

def get_session():
    session = current_session.get()
    if session is None:
        raise RuntimeError("no current session")
    return session
```

Request handler:

```python id="gqdz8f"
async def handle_request():
    session = open_session()
    token = current_session.set(session)
    try:
        await service()
    finally:
        current_session.reset(token)
        session.close()
```

This avoids passing `session` through every function, but it also hides a dependency. Use it deliberately.

## 50.18 Hidden Dependency Cost

Context variables are powerful because they avoid plumbing arguments everywhere.

That same property can make code harder to understand.

This function:

```python id="rk9yfz"
def save_user(user):
    session = get_session()
    session.save(user)
```

has a hidden dependency on the current context.

A clearer function may be:

```python id="g5mw33"
def save_user(session, user):
    session.save(user)
```

Design rule:

```text id="zt7a6i"
use context variables for cross-cutting execution context
avoid them for ordinary business data
```

Good uses:

```text id="5a8sxy"
request ID
trace span
auth principal
locale
deadline
transaction context
structured logging fields
```

Poor uses:

```text id="woxrp1"
ordinary function inputs
core domain data
mutable application state
configuration that should be explicit
```

## 50.19 Defaults

A `ContextVar` can have a default.

```python id="9lw0fi"
var = ContextVar("var", default="unset")
print(var.get())
```

Or `get()` can receive a default:

```python id="0t198e"
var = ContextVar("var")
print(var.get("fallback"))
```

Difference:

```text id="gd9q6r"
ContextVar default:
    stored on variable definition

get(default):
    used only for that get call
```

If neither exists and no value is set, `get()` raises `LookupError`.

## 50.20 Reset Discipline

Always reset scoped values.

Bad:

```python id="xh0p38"
def handle_request(id):
    request_id.set(id)
    process()
```

If the same task later handles another unit of work, the previous value may remain.

Better:

```python id="ung4ri"
def handle_request(id):
    token = request_id.set(id)
    try:
        process()
    finally:
        request_id.reset(token)
```

Tokens make context variable updates reversible.

## 50.21 Nested Values

Tokens support nesting.

```python id="q8lq03"
var = ContextVar("var", default="root")

t1 = var.set("A")
print(var.get())

t2 = var.set("B")
print(var.get())

var.reset(t2)
print(var.get())

var.reset(t1)
print(var.get())
```

Output:

```text id="msdfhe"
A
B
A
root
```

Each token restores the previous value for that context variable.

Reset tokens in reverse order.

## 50.22 Token Misuse

A token belongs to the context and variable that created it.

Do not use a token with the wrong variable.

```python id="e07gut"
a = ContextVar("a")
b = ContextVar("b")

token = a.set(1)
b.reset(token)
```

This raises an error.

Do not reset the same token twice.

```python id="t5xatx"
token = a.set(1)
a.reset(token)
a.reset(token)
```

Tokens are one-use restoration handles.

## 50.23 Mutable Values

A context variable can store a mutable object.

```python id="vhb927"
var = ContextVar("var")
var.set([])
var.get().append(1)
```

This is legal, but it can be dangerous.

The context binding points to the list. Mutating the list mutates the same object.

For request context, prefer immutable or controlled values.

Better:

```python id="szs9t5"
var.set(("req-1", "user-2"))
```

or store an object whose mutation rules are explicit.

Context variables isolate bindings, not all mutations of referenced objects.

## 50.24 Copying Context With Mutable Values

Copied contexts copy bindings, not deep object graphs.

```python id="6qjx43"
var = ContextVar("var")

items = []
var.set(items)

ctx = copy_context()
items.append(1)

ctx.run(lambda: print(var.get()))
```

The copied context sees the same list object.

Output:

```text id="1wwa5p"
[1]
```

Context copying is shallow with respect to stored values.

Use immutable values when isolation matters.

## 50.25 Context and Task Groups

When using task groups or similar structured concurrency, child tasks generally inherit the context at creation time.

```python id="k2nibt"
async def main():
    request_id.set("req-1")

    async with asyncio.TaskGroup() as tg:
        tg.create_task(worker())
```

The worker sees the request ID captured when the task was created.

If the parent changes the variable later, the already created task does not automatically track that change.

## 50.26 Context and `asyncio.to_thread`

`asyncio.to_thread` is designed to propagate context to the worker thread.

```python id="8qdhtt"
import asyncio
from contextvars import ContextVar

var = ContextVar("var", default="unset")

def blocking():
    print(var.get())

async def main():
    var.set("async-context")
    await asyncio.to_thread(blocking)

asyncio.run(main())
```

The blocking function sees the async context value.

This makes `to_thread` safer than manually starting a thread when context propagation matters.

## 50.27 Context and Executors

Context propagation to executor workers depends on the API and version behavior.

A safe explicit pattern is:

```python id="hy8jca"
import asyncio
from contextvars import copy_context

async def main():
    loop = asyncio.get_running_loop()
    ctx = copy_context()

    await loop.run_in_executor(None, ctx.run, blocking_function)
```

This ensures the callable runs inside the copied context.

Use explicit context capture when correctness depends on it.

## 50.28 Context and Generators

Context variables interact with generators according to where execution occurs.

A generator resumes in the context active when it is advanced.

This can matter when a generator is passed between execution contexts.

For most code, this is not visible. It becomes important in frameworks that schedule generators, coroutines, callbacks, or greenlet-like execution manually.

## 50.29 Context and Callbacks

Callbacks run in the context chosen by the scheduler.

In `asyncio`, callbacks scheduled with event loop APIs often capture or use context according to the event loop’s rules.

Framework authors must be careful to preserve context when moving work across callbacks, threads, or queues.

Application authors mostly see this through logging and tracing correctness.

## 50.30 Context and Greenlets

Some concurrency frameworks use greenlets or coroutines outside standard `asyncio`.

Context variable support depends on how the framework integrates with Python context management.

Frameworks that switch execution manually must preserve and restore contexts correctly.

This is an implementation responsibility for the scheduler.

## 50.31 Context Variables in Libraries

Libraries should avoid creating surprising global context requirements.

Good library use:

```text id="2kzjnu"
read current trace context if present
include request ID in logs if present
support explicit argument override
provide clear context manager for binding
```

Poor library use:

```text id="p4rs4v"
requires hidden context for core correctness
silently mutates context in unrelated functions
stores large mutable objects
fails if context is missing without clear error
```

A library should make context behavior explicit in its API.

## 50.32 Context Manager Pattern

A common pattern:

```python id="b6nvjm"
from contextlib import contextmanager
from contextvars import ContextVar

current_user = ContextVar("current_user", default=None)

@contextmanager
def user_context(user):
    token = current_user.set(user)
    try:
        yield
    finally:
        current_user.reset(token)
```

Usage:

```python id="yqdnai"
with user_context(user):
    run()
```

This gives callers a visible boundary for context changes.

## 50.33 Async Context Manager Pattern

```python id="78qpma"
from contextlib import asynccontextmanager
from contextvars import ContextVar

current_request = ContextVar("current_request", default=None)

@asynccontextmanager
async def request_context(request):
    token = current_request.set(request)
    try:
        yield
    finally:
        current_request.reset(token)
```

Usage:

```python id="3qo98v"
async with request_context(request):
    await handle()
```

This pattern works naturally with async request lifetimes.

## 50.34 Context Variables and Logging

Context variables are useful for structured logging.

```python id="28gwwu"
request_id = ContextVar("request_id", default="-")

def log(message):
    print(f"request_id={request_id.get()} message={message}")
```

Inside request handling:

```python id="orlr5m"
token = request_id.set("req-42")
try:
    log("start")
    await service()
    log("done")
finally:
    request_id.reset(token)
```

Every nested function can log with the current request ID.

Production logging systems often inject context variables through filters, adapters, or structured log processors.

## 50.35 Context Variables and Cancellation

Async cancellation can interrupt code.

Always reset context variables in `finally`.

```python id="74f90h"
token = request_id.set("req-1")
try:
    await work()
finally:
    request_id.reset(token)
```

If `work()` is cancelled, the `finally` block still runs.

Without `finally`, cancelled tasks may leave stale context values in reused execution contexts.

## 50.36 Context Variables and Testing

Tests using context variables should reset state.

Bad:

```python id="q4fj8f"
def test_one():
    request_id.set("test-one")
```

This can leak into later tests running in the same context.

Better:

```python id="alt147"
def test_one():
    token = request_id.set("test-one")
    try:
        assert request_id.get() == "test-one"
    finally:
        request_id.reset(token)
```

A fixture can manage this pattern.

## 50.37 Context Variables and Performance

Context variables are efficient enough for request context, tracing, and logging.

But they still have cost.

Avoid using them for extremely hot inner-loop data flow when an explicit local variable would be simpler and faster.

Good:

```python id="pcw2ht"
request_id.get()
```

inside logging or tracing.

Poor:

```python id="q72rje"
for item in millions:
    value = current_value.get()
    compute(value, item)
```

Pass hot-loop values explicitly.

## 50.38 CPython Internals

At the CPython level, context variables are integrated with thread state and task scheduling.

A thread has a current context.

Async task machinery saves and restores context as tasks are scheduled.

Conceptually:

```text id="693v2l"
thread state
    current context

asyncio task
    saved context

when task runs:
    thread state's current context = task context

when task yields:
    task saves updated context
```

This is why context variables can behave as task-local state on top of a single OS thread.

## 50.39 Context and Frames

Context variable lookup does not search Python stack frames.

It searches the current context.

This is different from local variables, globals, and closures.

```text id="dyb6tb"
local variable:
    stored in frame

global variable:
    stored in module dictionary

closure variable:
    stored in cell

context variable:
    stored in current Context
```

This distinction matters when reasoning about execution state.

## 50.40 Context Propagation Boundaries

Be alert at boundaries where context may or may not propagate:

```text id="5r1wer"
creating an asyncio task
calling into a thread
using an executor
scheduling callbacks
crossing process boundaries
calling native code
using custom schedulers
using greenlets
```

When correctness depends on context, test the boundary explicitly.

## 50.41 Cross-Process Behavior

Context variables do not automatically cross process boundaries.

If you use multiprocessing, subprocesses, or external workers, propagate context explicitly.

Example:

```python id="9p4kgq"
payload = {
    "request_id": request_id.get(),
    "data": data,
}
send_to_worker(payload)
```

The receiving process can bind the value to its own context.

## 50.42 Cross-Interpreter Behavior

Subinterpreters have separate interpreter state. Context variables and their values should be treated as interpreter-local unless an explicit communication mechanism transfers data.

Pass neutral data, such as strings or bytes, across interpreter boundaries.

Do not assume context objects or context variable bindings can be shared directly.

## 50.43 API Design With Context Variables

A good API often supports both explicit and implicit context.

Example:

```python id="9fo7ym"
current_request_id = ContextVar("current_request_id", default=None)

def emit_event(name, *, request_id=None):
    if request_id is None:
        request_id = current_request_id.get()
    write_event(name, request_id=request_id)
```

This lets callers pass explicit values when needed, while still supporting context-based defaults.

This is better than making hidden context mandatory.

## 50.44 Common Bugs

| Bug | Cause | Fix |
|---|---|---|
| Value leaks between operations | Missing token reset | Use `try/finally` |
| Worker thread sees default | Context not propagated | Use `copy_context` or `asyncio.to_thread` |
| Async tasks see old value | Context captured at task creation | Set value before creating task |
| Mutation leaks across contexts | Stored mutable object | Store immutable values or copy |
| Tests affect each other | Context not reset | Use fixtures or tokens |
| Hidden dependency | Function reads context silently | Accept explicit argument override |
| Executor loses tracing | Context not copied | Run executor callable under copied context |

## 50.45 Design Rules

Use context variables for execution context, not ordinary data flow.

Set values at clear boundaries, such as request start, task start, or transaction start.

Always reset using tokens.

Prefer immutable values.

Copy context explicitly when crossing thread or scheduler boundaries.

Do not rely on context variables for security isolation.

Provide explicit parameters for important business data.

Use context managers to make scope visible.

## 50.46 Minimal Mental Model

Use this model:

```text id="4lqn5r"
A ContextVar is a key.

The current Context maps ContextVars to values.

Each thread has a current Context.

Async tasks save and restore their Context as they run.

Creating a task usually copies the current Context.

Setting a ContextVar changes the current Context.

A token restores the previous binding.

Context values do not automatically cross threads, processes, or interpreters unless explicitly propagated.
```

## 50.47 Key Points

Context variables provide logical-context-local state.

They solve the problem that thread-local storage cannot solve in async programs.

A `ContextVar` stores values in the current `Context`, not on the variable object itself.

`set()` returns a token that should be reset.

`copy_context()` captures the current context.

Async tasks generally capture context when created.

Threads require explicit context propagation unless an API does it for you.

Mutable values can still be shared if the binding points to the same object.

Use context variables for request IDs, trace spans, auth context, locale, deadlines, and similar cross-cutting execution state.
