When building FastAPI applications, you'll often need to share data across different parts of your request lifecycle i.e. middleware, dependencies, and route handlers. FastAPI provides two primary mechanisms for this: request.state
and context variables (contextvars). But when should you use each one?
Understanding request.state 📦
request.state
is a simple namespace object attached to each FastAPI request. It's designed to store arbitrary data that needs to be shared during the processing of a single request.
Basic Usage 🔧
from fastapi import FastAPI, Request, Depends
app = FastAPI()
@app.middleware("http")
async def add_user_info(request: Request, call_next):
# Store data in request.state
request.state.user_id = "user_123"
request.state.request_time = time.time()
response = await call_next(request)
return response
@app.get("/profile")
async def get_profile(request: Request):
# Access data from request.state
user_id = request.state.user_id
return {"user_id": user_id, "message": "User profile"}
Pros of request.state ✅
- ✅ Simple and straightforward
- ✅ Explicitly tied to the request object
- ✅ Easy to understand and debug
- ✅ No additional imports needed
- ✅ Works well with dependency injection
Cons of request.state ❌
- ❌ Requires passing the
Request
object around - ❌ Not accessible in deeply nested functions without explicit passing
- ❌ Can become verbose in complex applications
Understanding Context Variables 🌍
Context variables (contextvars) provide a way to store data that's automatically available throughout the execution context of a request, without explicitly passing it around.
Basic Usage 🚀
from contextvars import ContextVar
from fastapi import FastAPI, Request
import time
# Define context variables
user_id_var: ContextVar[str] = ContextVar('user_id')
request_time_var: ContextVar[float] = ContextVar('request_time')
app = FastAPI()
@app.middleware("http")
async def add_user_info(request: Request, call_next):
# Set context variables
user_id_var.set("user_123")
request_time_var.set(time.time())
response = await call_next(request)
return response
@app.get("/profile")
async def get_profile():
# Access context variables from anywhere
user_id = user_id_var.get()
request_time = request_time_var.get()
return {
"user_id": user_id,
"request_time": request_time,
"message": "User profile"
}
Pros of Context Variables ✅
- ✅ Available anywhere in the execution context
- ✅ No need to pass request object around
- ✅ Cleaner function signatures
- ✅ Automatic cleanup after request completion
- ✅ Thread-safe and async-safe
Cons of Context Variables ❌
- ❌ Less explicit than request.state
- ❌ Harder to debug and trace
- ❌ Requires additional setup and imports
- ❌ Can make testing more complex
Advanced Patterns 🎨
Using Context Variables with Dependencies 🔗
from contextvars import ContextVar
from fastapi import FastAPI, Depends, HTTPException
from typing import Optional
current_user_var: ContextVar[Optional[dict]] = ContextVar('current_user', default=None)
app = FastAPI()
async def get_current_user() -> dict:
user = current_user_var.get()
if not user:
raise HTTPException(status_code=401, detail="User not authenticated")
return user
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# Simulate user authentication
token = request.headers.get("authorization")
if token:
user = {"id": "123", "name": "John Doe"} # Simulate user lookup
current_user_var.set(user)
response = await call_next(request)
return response
@app.get("/protected")
async def protected_route(user: dict = Depends(get_current_user)):
return {"message": f"Hello {user['name']}!"}
Combining Both Approaches 🤝
from contextvars import ContextVar
from fastapi import FastAPI, Request
import uuid
# Context var for request ID (global access)
request_id_var: ContextVar[str] = ContextVar('request_id')
app = FastAPI()
@app.middleware("http")
async def request_middleware(request: Request, call_next):
# Generate request ID and store in both places
request_id = str(uuid.uuid4())
# Store in context var for global access
request_id_var.set(request_id)
# Store in request.state for explicit access
request.state.request_id = request_id
request.state.start_time = time.time()
response = await call_next(request)
return response
Performance Considerations ⚡
Memory Usage
- request.state: Minimal overhead, data is garbage collected with the request 🗑️
- contextvars: Slightly higher overhead due to context management, but still very efficient 💪
When to Use What? 🤔
Use request.state when:
- 🎯 You want explicit, traceable data flow
- 🎯 Building simple applications
- 🎯 You're already passing Request objects around
- 🎯 You need to store request-specific metadata
- 🎯 Debugging and testing simplicity is important
Use context variables when:
- 🎯 You have deeply nested function calls
- 🎯 You want cleaner function signatures
- 🎯 Building complex applications with many layers
- 🎯 You need global access to request-scoped data
- 🎯 Working with third-party libraries that need access to request data
Real-World Example: Logging System 📝
Here's how you might implement a request logging system with both approaches:
With Context Variables 🌐
from contextvars import ContextVar
import logging
import uuid
request_id_var: ContextVar[str] = ContextVar('request_id')
logger = logging.getLogger(__name__)
class RequestLogger:
@staticmethod
def info(message: str):
request_id = request_id_var.get("unknown")
logger.info(f"[{request_id}] {message}")
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
request_id_var.set(str(uuid.uuid4()))
RequestLogger.info(f"Request started: {request.method} {request.url}")
response = await call_next(request)
RequestLogger.info(f"Request completed: {response.status_code}")
return response
@app.get("/users/{user_id}")
async def get_user(user_id: int):
RequestLogger.info(f"Fetching user {user_id}")
# Your business logic here
return {"user_id": user_id}
With request.state 📋
import logging
import uuid
logger = logging.getLogger(__name__)
def log_with_request_id(request: Request, message: str):
request_id = getattr(request.state, 'request_id', 'unknown')
logger.info(f"[{request_id}] {message}")
@app.get("/users/{user_id}")
async def get_user(user_id: int, request: Request):
log_with_request_id(request, f"Fetching user {user_id}")
# Your business logic here
return {"user_id": user_id}
Have you used both approaches in your FastAPI projects? Share your experiences in the comments below! 👇
Top comments (0)