Context Variables Intermediate¶
contextvars (Python 3.7+) provides variables whose values are local to a logical context — a thread or an asyncio task. They solve the problem of passing request-scoped data (request IDs, the current user, locale) without threading it through every function argument, and unlike globals they don't leak between concurrent tasks.
Basic get / set¶
Basic get / set in Context Variables — what it is and when to use it.
from contextvars import ContextVar
request_id: ContextVar[str] = ContextVar("request_id", default="-")
print(request_id.get()) # - (the default)
request_id.set("abc123")
print(request_id.get()) # abc123
Tokens and reset¶
Tokens and reset in Context Variables — what it is and when to use it.
set() returns a Token that restores the previous value — the basis for scoped overrides:
from contextvars import ContextVar
level = ContextVar("level", default="INFO")
token = level.set("DEBUG")
print(level.get()) # DEBUG
level.reset(token)
print(level.get()) # INFO (restored)
Why not just a global?¶
A core question explored in Context Variables: Why not just a global.
A plain global is shared by every task. A ContextVar gives each asyncio task its own value, so concurrent requests don't clobber each other.
import asyncio
from contextvars import ContextVar
user = ContextVar("user")
async def handle(name):
user.set(name)
await asyncio.sleep(0.01) # yield to the event loop
return user.get() # still *this* task's value
async def main():
return await asyncio.gather(handle("alice"), handle("bob"))
print(asyncio.run(main())) # ['alice', 'bob'] — no cross-talk
With a global, the interleaving await would let one task overwrite the other.
Running inside a copied context¶
Running inside a copied context in Context Variables — what it is and when to use it.
copy_context() snapshots the current values and runs a callable in that isolated copy — changes inside don't escape:
import contextvars
v = contextvars.ContextVar("v", default=0)
def bump_and_read():
v.set(99)
return v.get()
ctx = contextvars.copy_context()
print(ctx.run(bump_and_read)) # 99 (inside the copied context)
print(v.get()) # 0 (outer context unchanged)
A practical logging-context pattern¶
A practical logging-context pattern — a key concept in Context Variables.
from contextvars import ContextVar
correlation_id = ContextVar("correlation_id", default="none")
def log(message):
return f"[{correlation_id.get()}] {message}"
correlation_id.set("req-42")
print(log("processing")) # [req-42] processing
Middleware sets the id once per request; every log call deep in the stack picks it up automatically.
contextvars vs threading.local
threading.local is per-thread only. ContextVar is per-thread and per-asyncio-task, and it propagates correctly across await — making it the right tool for async apps.
Practice exercises¶
- Create a
localeContextVar and a function that formats a greeting using its value. - Use a token to temporarily override a setting inside a
with-style helper, then restore it. - Launch three asyncio tasks that each set and read their own
ContextVarvalue. - Use
copy_context().run()to call a function without letting its context changes leak out.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.