An asyncio task receives a copy of the current context when it is created unless a context is supplied.
Python ContextVar tasks: capture request context at task creation
Operation contract
The parent assigns tenant north-47 and creates a task waiting on a gate. Before opening the gate, the parent changes its own tenant to south-47. The task still reads the value captured at creation. That is useful for request labels, but only when task creation happens under the intended request context.
Failure and ownership boundary
ContextVar values are not an authorization decision. A copied reference to a mutable object can still alias that object, so store immutable identifiers rather than a shared mutable request bag. Explicitly reset tokens in the same context after a request. Thread and process handoffs have their own propagation rules.
Working program
import asyncio
from contextvars import ContextVar
tenant_id = ContextVar("tenant_id")
async def read_tenant(gate):
await gate.wait()
return tenant_id.get()
async def main():
token = tenant_id.set("north-47")
try:
gate = asyncio.Event()
reader = asyncio.create_task(read_tenant(gate))
tenant_id.set("south-47")
gate.set()
print("task", await reader)
print("parent", tenant_id.get())
finally:
tenant_id.reset(token)
asyncio.run(main())Output
task north-47
parent south-47Costs and limits
Creating a task copies context bindings. Large mutable objects referenced by those bindings can outlive a request if tasks remain alive.
Common Mistakes
- Changing the parent context after task creation does not rewrite the task copy.
- Context propagation does not authorize a tenant operation.
- A copied context binding can still reference mutable shared state.
