Context Variables (contextvars)¶
contextvars provides context-local state that works correctly with both threads and async tasks.
Why contextvars?¶
The Problem with threading.local in Async¶
import threading
import asyncio
# threading.local() doesn't work properly with async
local = threading.local()
async def task(name):
local.name = name
await asyncio.sleep(0.1)
# BUG: May see wrong value after await!
print(f"Task: {local.name}")
async def main():
await asyncio.gather(
task("task1"),
task("task2")
)
# Both might print "task2" because they share the same thread
The Solution: contextvars¶
import contextvars
import asyncio
# contextvars works correctly with async
name_var = contextvars.ContextVar('name')
async def task(name):
name_var.set(name)
await asyncio.sleep(0.1)
# Correct: Each task sees its own value
print(f"Task: {name_var.get()}")
async def main():
await asyncio.gather(
task("task1"),
task("task2")
)
# Output: "task1" and "task2" correctly
asyncio.run(main())
Basic Usage¶
Creating and Using ContextVar¶
import contextvars
# Create a context variable
request_id = contextvars.ContextVar('request_id', default=None)
# Set value
request_id.set('req-123')
# Get value
print(request_id.get()) # 'req-123'
# Get with default (if not set)
other_var = contextvars.ContextVar('other')
print(other_var.get('default')) # 'default'
print(other_var.get()) # Raises LookupError if no default
Token-Based Reset¶
import contextvars
var = contextvars.ContextVar('var', default='initial')
# set() returns a token
token = var.set('new_value')
print(var.get()) # 'new_value'
# Reset to previous value using token
var.reset(token)
print(var.get()) # 'initial'
Context Management¶
Working with Context Objects¶
import contextvars
var = contextvars.ContextVar('var')
# Get current context
ctx = contextvars.copy_context()
# Run function in context
def show_value():
return var.get('not set')
# Set in current context
var.set('hello')
print(ctx.run(show_value)) # 'not set' (ctx was copied before set)
# Create new context with modifications
ctx2 = contextvars.copy_context()
print(ctx2.run(show_value)) # 'hello'
Running in Isolated Context¶
import contextvars
request_id = contextvars.ContextVar('request_id')
def process_request(req_id):
request_id.set(req_id)
# ... do work ...
return request_id.get()
# Run in isolated context
ctx = contextvars.copy_context()
result = ctx.run(process_request, 'req-123')
# Original context unaffected
print(request_id.get('not set')) # 'not set'
Async Task Context¶
Automatic Context Copying¶
import asyncio
import contextvars
user_id = contextvars.ContextVar('user_id')
async def get_user_data():
# Each task has its own copy of context
uid = user_id.get()
await asyncio.sleep(0.1)
return f"Data for user {uid}"
async def handle_request(uid):
user_id.set(uid)
# Task created here inherits context
task = asyncio.create_task(get_user_data())
return await task
async def main():
results = await asyncio.gather(
handle_request('user-1'),
handle_request('user-2')
)
print(results)
# ['Data for user user-1', 'Data for user user-2']
asyncio.run(main())
Context in Callbacks¶
import asyncio
import contextvars
var = contextvars.ContextVar('var')
async def main():
var.set('main_value')
loop = asyncio.get_running_loop()
def callback():
# Callback runs in context where it was scheduled
print(f"Callback: {var.get()}")
# Schedule callback - captures current context
loop.call_soon(callback)
await asyncio.sleep(0.1)
asyncio.run(main()) # Prints: "Callback: main_value"
Practical Examples¶
1. Request Context in Web Framework¶
import contextvars
from contextlib import contextmanager
# Context variables for request
request_id = contextvars.ContextVar('request_id')
current_user = contextvars.ContextVar('current_user')
@contextmanager
def request_context(req_id, user):
"""Set request context for the duration of handling."""
token_id = request_id.set(req_id)
token_user = current_user.set(user)
try:
yield
finally:
request_id.reset(token_id)
current_user.reset(token_user)
def get_request_id():
return request_id.get(None)
def get_current_user():
return current_user.get(None)
# Usage in async handler
async def handle_request(req_id, user):
with request_context(req_id, user):
# All code here sees the context
result = await process_request()
log_request() # Can access request_id
return result
def log_request():
print(f"Request {get_request_id()} by {get_current_user()}")
2. Database Transaction Context¶
import contextvars
from contextlib import asynccontextmanager
_transaction = contextvars.ContextVar('transaction', default=None)
@asynccontextmanager
async def transaction(conn):
"""Provide transaction context."""
tx = await conn.begin()
token = _transaction.set(tx)
try:
yield tx
await tx.commit()
except Exception:
await tx.rollback()
raise
finally:
_transaction.reset(token)
def get_transaction():
"""Get current transaction or raise."""
tx = _transaction.get()
if tx is None:
raise RuntimeError("No active transaction")
return tx
async def save_user(user):
tx = get_transaction()
await tx.execute("INSERT INTO users ...")
3. Logging Context¶
import contextvars
import logging
# Context for logging
log_context = contextvars.ContextVar('log_context', default={})
class ContextFilter(logging.Filter):
def filter(self, record):
ctx = log_context.get()
for key, value in ctx.items():
setattr(record, key, value)
return True
def with_log_context(**kwargs):
"""Add context to all logs in this scope."""
current = log_context.get().copy()
current.update(kwargs)
return log_context.set(current)
# Usage
async def handle_request(request_id, user_id):
token = with_log_context(request_id=request_id, user_id=user_id)
try:
logger.info("Processing request") # Includes context
await do_work()
logger.info("Request completed")
finally:
log_context.reset(token)
4. Timeout Context¶
import contextvars
import asyncio
import time
deadline = contextvars.ContextVar('deadline', default=None)
def get_remaining_time():
"""Get remaining time until deadline."""
dl = deadline.get()
if dl is None:
return float('inf')
return max(0, dl - time.time())
@asynccontextmanager
async def timeout(seconds):
"""Set deadline for operations."""
new_deadline = time.time() + seconds
current = deadline.get()
# Use earliest deadline
if current is not None:
new_deadline = min(new_deadline, current)
token = deadline.set(new_deadline)
try:
yield
finally:
deadline.reset(token)
async def fetch_with_deadline(url):
remaining = get_remaining_time()
if remaining <= 0:
raise asyncio.TimeoutError("Deadline exceeded")
async with aiohttp.ClientSession() as session:
async with asyncio.timeout(remaining):
async with session.get(url) as r:
return await r.text()
Comparison: threading.local vs contextvars¶
| Feature | threading.local | contextvars |
|---|---|---|
| Thread isolation | ✅ | ✅ |
| Async task isolation | ❌ | ✅ |
| Automatic context copy | ❌ | ✅ |
| Reset to previous value | ❌ | ✅ (tokens) |
| Explicit context objects | ❌ | ✅ |
| Python version | 2.4+ | 3.7+ |
Best Practices¶
1. Use Meaningful Names¶
# Good: Descriptive names
request_id = contextvars.ContextVar('request_id')
current_user = contextvars.ContextVar('current_user')
# Bad: Generic names
var1 = contextvars.ContextVar('var1')
2. Provide Defaults When Appropriate¶
# With default
log_level = contextvars.ContextVar('log_level', default='INFO')
# Without default (requires explicit set)
auth_token = contextvars.ContextVar('auth_token')
3. Always Reset in Finally¶
token = var.set('value')
try:
# ... do work ...
finally:
var.reset(token)
# Or use context manager
@contextmanager
def scoped_var(var, value):
token = var.set(value)
try:
yield
finally:
var.reset(token)
4. Document Context Dependencies¶
def process_order():
"""Process the current order.
Requires:
- current_user context variable to be set
- request_id context variable to be set
"""
user = current_user.get() # Raises if not set
req_id = request_id.get()
Key Takeaways¶
- Use
contextvarsfor state that should be isolated per async task - Each
asyncio.create_task()gets a copy of the current context - Use tokens from
set()toreset()values properly - Works for both sync and async code
- Replaces
threading.local()for async-aware context - Essential for request context, transactions, logging in async apps