contextvars Module Complexity¶
The contextvars module provides context variables for managing state in concurrent and async code while maintaining isolation between execution contexts.
Classes & Functions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
ContextVar(name) |
O(1) | O(1) | Create context variable |
ContextVar.set(value) |
O(1) | O(1) | Returns token for reset |
ContextVar.get() |
O(1) | O(1) | Returns default if not set |
copy_context() |
O(n) | O(n) | Copy context, n = variable count |
Context.run(fn, *args) |
O(1) | O(1) | Run function in context |
Creating Context Variables¶
Time Complexity: O(1)¶
from contextvars import ContextVar
# Create variable: O(1)
request_id = ContextVar('request_id') # O(1)
# With default value: O(1)
user_id = ContextVar('user_id', default=None) # O(1)
# Each variable is a singleton
# Multiple references to same name share state
request_id2 = ContextVar('request_id') # Different object, shares context value
Space Complexity: O(1)¶
from contextvars import ContextVar
# Variable object is small: O(1)
request_id = ContextVar('request_id') # O(1) space
Setting and Getting Values¶
Time Complexity: O(1)¶
from contextvars import ContextVar
request_id = ContextVar('request_id')
# Set value in context: O(1)
token = request_id.set('req-123') # O(1)
# Get value from context: O(1)
current_id = request_id.get() # O(1)
# Get with default: O(1)
default_id = request_id.get('no-id') # O(1)
# Reset to previous: O(1)
request_id.reset(token) # O(1)
Space Complexity: O(1)¶
from contextvars import ContextVar
# Each set stores one value: O(1)
request_id = ContextVar('request_id')
request_id.set('req-1') # O(1) space
request_id.set('req-2') # O(1) space (replaces)
Context Copying¶
Time Complexity: O(n)¶
Where n = number of context variables set.
from contextvars import ContextVar, copy_context
request_id = ContextVar('request_id')
user_id = ContextVar('user_id')
session_id = ContextVar('session_id')
# Set values: O(1) per set
request_id.set('req-123') # O(1)
user_id.set('user-456') # O(1)
session_id.set('sess-789') # O(1)
# Copy context: O(n) where n = 3 variables
ctx = copy_context() # O(n)
# Space: O(n) for copy
# Copying is relatively cheap, O(n) where n is usually small
Space Complexity: O(n)¶
from contextvars import copy_context
# Copying creates new context with all values
ctx = copy_context() # O(n) space for n variables
Running in Contexts¶
Time Complexity: O(1)¶
from contextvars import ContextVar, copy_context
request_id = ContextVar('request_id')
# Set in current context
request_id.set('req-main')
# Copy context: O(n)
ctx = copy_context() # O(n)
# Run function in copied context: O(1) to switch
def task():
current_id = request_id.get() # O(1)
return current_id
# Run in context: O(1) operation
# (function execution time is separate)
result = ctx.run(task) # O(1) + function time
Space Complexity: O(1) for switching¶
from contextvars import copy_context
# No additional space to switch contexts
# O(1) operation
ctx = copy_context() # O(n) space for context
result = ctx.run(some_function) # O(1) switching space
Common Patterns¶
Request Context in Web Applications¶
from contextvars import ContextVar
from datetime import datetime
# Define context variables
request_id = ContextVar('request_id')
user_id = ContextVar('user_id', default=None)
request_time = ContextVar('request_time')
# In request handler
def handle_request(request):
"""Handle HTTP request."""
# Set context variables: O(1) each
request_id.set(request.id) # O(1)
user_id.set(request.user_id) # O(1)
request_time.set(datetime.now()) # O(1)
# Call business logic
result = process_request(request) # Can access context vars
return result
def process_request(request):
"""Business logic that accesses context."""
# Get current request_id: O(1)
current_req_id = request_id.get()
# Log with context
log(f"Processing {current_req_id}")
# Call other functions that can access context
validate(request) # Can use request_id without passing it
return result
def validate(request):
"""Access context variables implicitly."""
# Get from context without parameter passing: O(1)
req_id = request_id.get()
print(f"Validating {req_id}")
Async Task Context¶
import asyncio
from contextvars import ContextVar
task_id = ContextVar('task_id')
async def process_task(task_data):
"""Process async task."""
# Set context for this task: O(1)
task_id.set(task_data['id'])
# Each await preserves context automatically
result = await fetch_data() # Context preserved
return process_result(result) # Can access task_id: O(1)
async def fetch_data():
"""Access context in async function."""
# Context is inherited from caller: O(1)
current_task_id = task_id.get()
# Async operations preserve context
await asyncio.sleep(1)
return {"result": "data"}
async def main():
"""Run multiple tasks with separate contexts."""
tasks = [
asyncio.create_task(process_task({"id": f"task-{i}"}))
for i in range(5)
]
# Each task has separate context automatically
results = await asyncio.gather(*tasks) # O(n)
Thread Context Isolation¶
from contextvars import ContextVar
from threading import Thread
# Note: contextvars are isolated per thread/async task
# Not inherited by default in threads
request_id = ContextVar('request_id')
def thread_worker(task_id):
"""Worker function in thread."""
# Set in thread context: O(1)
# This does NOT affect main thread context
request_id.set(f"thread-{task_id}")
# Get from thread context: O(1)
current_id = request_id.get()
print(f"Worker: {current_id}")
def main():
"""Main thread context."""
# Set in main: O(1)
request_id.set('main-thread')
# Create and start thread
thread = Thread(target=thread_worker, args=(1,))
thread.start()
# Main thread context unchanged: O(1) to get
current_id = request_id.get()
print(f"Main: {current_id}") # Still 'main-thread'
thread.join()
Context Propagation to Threads¶
from contextvars import ContextVar, copy_context
from threading import Thread
request_id = ContextVar('request_id')
def thread_worker(ctx, task_id):
"""Worker runs in provided context."""
# Run in copied context: O(1) switch
def work():
# Now can access context: O(1)
current_id = request_id.get()
print(f"Worker {task_id}: {current_id}")
return f"result-{task_id}"
return ctx.run(work) # O(1) to switch context
def main():
"""Set up context and run in thread."""
# Set main context: O(1)
request_id.set('main-request')
# Copy context: O(n)
ctx = copy_context()
# Pass context to thread
thread = Thread(target=thread_worker, args=(ctx, 1))
thread.start()
thread.join()
Performance Characteristics¶
Context Variable Access¶
from contextvars import ContextVar
var = ContextVar('var')
# Set is fast: O(1)
var.set('value') # O(1)
# Get is fast: O(1)
value = var.get() # O(1)
# No performance overhead vs global variable
# (except for isolation benefit)
Context Copying¶
from contextvars import copy_context, ContextVar
# Copying has cost: O(n)
ctx = copy_context() # O(n) where n = variables set
# But only needed occasionally (not per operation)
# Usually done once per request/task
# Usually n is small (< 10 variables)
# So O(n) is acceptable
Best Practices¶
from contextvars import ContextVar
# Good: Create variables at module level
request_id = ContextVar('request_id')
user_id = ContextVar('user_id') # O(1) at import time
# Avoid: Creating variables in functions
def handle_request():
# Creating in function: inefficient
temp_var = ContextVar('temp') # O(1) but wasteful
temp_var.set('value')
# Good: Set once per request/task
def handle_request(request):
request_id.set(request.id) # O(1)
# Don't set repeatedly
# Avoid: Setting repeatedly in loop
def process_items(items):
for item in items:
request_id.set(item.id) # O(n) sets, inefficient
process(item)
# Good: Copy context for new execution path
async def task():
ctx = copy_context() # O(n) once
result = ctx.run(function) # O(1) to switch
# Avoid: Copying context repeatedly
for i in range(1000):
ctx = copy_context() # O(n*1000) - wasteful!
ctx.run(function)
Async vs Threading¶
from contextvars import ContextVar
import asyncio
from threading import Thread
var = ContextVar('var')
async def async_task():
"""Async tasks share context efficiently."""
var.set('value-1')
# Create subtask: context inherited automatically
subtask = asyncio.create_task(async_subtask())
result = await subtask
return result
# Total: O(1) context operations
async def async_subtask():
"""Access parent context: O(1)."""
value = var.get() # O(1) - inherits from parent
return value
def thread_task():
"""Threads require explicit context passing."""
from contextvars import copy_context
var.set('value-1')
# Must copy context: O(n)
ctx = copy_context()
# Pass to thread
thread = Thread(target=thread_subtask, args=(ctx,))
thread.start()
thread.join()
# Total: O(n) for copying
def thread_subtask(ctx):
"""Must run in copied context."""
result = ctx.run(lambda: var.get()) # O(1) to access
return result
Version Notes¶
- Python 3.7+: contextvars module introduced
- Python 3.11+: Enhanced performance
- Python 3.13+: Additional features
Related Documentation¶
- asyncio Module - Async/await with context support
- threading Module - Threading (context not automatic)
- concurrent.futures Module - Executors