DEV Community

Akarshan Gandotra
Akarshan Gandotra

Posted on

FastAPI: request.state vs Context Variables - When to Use What? πŸš€

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"} 
Enter fullscreen mode Exit fullscreen mode

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" } 
Enter fullscreen mode Exit fullscreen mode

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']}!"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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} 
Enter fullscreen mode Exit fullscreen mode

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} 
Enter fullscreen mode Exit fullscreen mode

Have you used both approaches in your FastAPI projects? Share your experiences in the comments below! πŸ‘‡

Top comments (0)