FastAPI Dependency Classes
Introduction
In FastAPI, dependency injection is a powerful mechanism that allows you to declare shared dependencies across your application. While simple functions work well as dependencies for basic use cases, dependency classes provide a more structured approach for complex scenarios. These classes encapsulate logic, maintain state, and offer a clean, object-oriented way to define reusable components.
In this tutorial, we'll explore how to create and use dependency classes in FastAPI, understanding why they're beneficial, and seeing practical examples of when to use them over simple function dependencies.
What are Dependency Classes?
Dependency classes in FastAPI are simply Python classes that define a __call__
method, making the class instances callable like functions. When used as dependencies, FastAPI calls these instances, invoking their __call__
method to retrieve the dependency value.
Basic Structure of a Dependency Class
class MyDependency:
def __init__(self):
# Initialization code here
self.some_value = "Hello World"
def __call__(self):
# This method gets called when the dependency is resolved
return self.some_value
Why Use Dependency Classes?
Dependency classes offer several advantages over simple function dependencies:
- State Management: Classes can maintain state between initializations
- Encapsulation: Group related operations and data together
- Inheritance: Use object-oriented patterns like inheritance and composition
- Testing: Easier to mock and test with dependency injection frameworks
- Configuration: More flexible initialization with complex configuration
Basic Example: Creating a Simple Dependency Class
Let's create a basic dependency class that provides database session management:
from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
app = FastAPI()
class DatabaseSessionDependency:
def __init__(self):
# Connection setup would happen here in a real application
self.connection_string = "sqlite:///./test.db"
print(f"Initializing DB connection to: {self.connection_string}")
def __call__(self):
# In a real app, this would create and return a session
print("Creating a new database session")
return Session() # Simplified for example
# Create an instance of the dependency class
get_db = DatabaseSessionDependency()
@app.get("/items/")
def read_items(db: Session = Depends(get_db)):
# Use the session from the dependency
return {"database": "connected", "session_id": id(db)}
When this application runs:
- The
DatabaseSessionDependency
class is instantiated once when definingget_db
- Each request calls the
__call__
method to get a fresh database session - The dependency injects this session into the route function
Advanced Usage: Parameterized Dependency Classes
One of the key benefits of dependency classes is the ability to parameterize them during initialization:
class ConfiguredDependency:
def __init__(self, prefix: str, required: bool = True):
self.prefix = prefix
self.required = required
def __call__(self, query_param: str = None):
if self.required and query_param is None:
raise ValueError("Query parameter is required")
if query_param:
return f"{self.prefix}: {query_param}"
return f"{self.prefix}: no value provided"
# Create two different instances with different configurations
get_required_item = ConfiguredDependency(prefix="ITEM", required=True)
get_optional_item = ConfiguredDependency(prefix="OPTIONAL", required=False)
@app.get("/required/")
def read_required(item_data: str = Depends(get_required_item)):
return {"item_data": item_data}
@app.get("/optional/")
def read_optional(item_data: str = Depends(get_optional_item)):
return {"item_data": item_data}
This example shows how we can create multiple instances of the same dependency class with different configurations, reusing the core logic while adapting the behavior.
Practical Example: Authentication Dependency Class
Let's build a more practical example - an authentication dependency for verifying API keys:
from fastapi import Depends, FastAPI, Header, HTTPException
from typing import Optional
app = FastAPI()
class APIKeyValidator:
def __init__(self, api_keys: list = None, auto_error: bool = True):
self.api_keys = api_keys or ["valid_key1", "valid_key2"]
self.auto_error = auto_error
def __call__(self, x_api_key: Optional[str] = Header(None)):
if x_api_key in self.api_keys:
return x_api_key
if self.auto_error:
raise HTTPException(
status_code=403,
detail="Invalid or missing API Key"
)
return None
# Standard API key validator that will raise exceptions
validate_api_key = APIKeyValidator()
# Silent validator that will return None instead of raising exceptions
optional_api_key = APIKeyValidator(auto_error=False)
@app.get("/protected/")
def protected_route(api_key: str = Depends(validate_api_key)):
return {"message": "Access granted", "key_used": api_key}
@app.get("/public/")
def public_route(api_key: Optional[str] = Depends(optional_api_key)):
if api_key:
return {"message": "Authenticated access", "key_used": api_key}
return {"message": "Public access"}
The above example demonstrates how dependency classes can encapsulate authentication logic while allowing for different behaviors based on configuration.
Dependency Classes with Sub-dependencies
Dependency classes can also depend on other dependencies, creating a chain of dependencies:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
class UserFinder:
def __call__(self, user_id: str = None):
# In a real app, this would query a database
if not user_id:
return None
return {"id": user_id, "name": f"User {user_id}"}
class UserPermissionsChecker:
def __init__(self):
# This is a dependency that depends on another dependency
pass
def __call__(self, user: dict = Depends(UserFinder())):
if not user:
raise HTTPException(status_code=401, detail="User not authenticated")
# In a real app, you'd check permissions in a database
return {
"user": user,
"permissions": ["read", "write"]
}
user_permissions = UserPermissionsChecker()
@app.get("/dashboard/")
def dashboard(permissions: dict = Depends(user_permissions)):
return {
"dashboard": "user data",
"user": permissions["user"]["name"],
"can_edit": "write" in permissions["permissions"]
}
This pattern allows for composing complex dependency chains while keeping each component focused and reusable.
Dependency Classes with Async Support
FastAPI fully supports async/await, and your dependency classes can be asynchronous too:
import asyncio
from fastapi import Depends, FastAPI
app = FastAPI()
class AsyncDataLoader:
def __init__(self, delay: float = 1.0):
self.delay = delay
async def __call__(self):
# Simulate an asynchronous operation (like a database query)
await asyncio.sleep(self.delay)
return {"data": "This was loaded asynchronously", "delay": self.delay}
# Create different loaders with different simulated delays
fast_loader = AsyncDataLoader(0.1)
slow_loader = AsyncDataLoader(2.0)
@app.get("/fast-data/")
async def get_fast_data(data: dict = Depends(fast_loader)):
return {"message": "Fast data loaded", **data}
@app.get("/slow-data/")
async def get_slow_data(data: dict = Depends(slow_loader)):
return {"message": "Slow data loaded", **data}
This example shows how dependency classes can encapsulate asynchronous operations, making your code cleaner while preserving asynchronous execution.
Best Practices for Dependency Classes
When working with dependency classes in FastAPI, keep these best practices in mind:
- Single Responsibility: Each dependency class should focus on one specific concern
- Reusability: Design classes to be reusable across different routes
- Configuration: Use
__init__
for configuration and__call__
for execution logic - Error Handling: Handle errors gracefully with appropriate HTTP exceptions
- Testing: Design classes that are easy to mock and test
- Documentation: Add docstrings to clearly explain the purpose and usage
- Type Hints: Use type annotations for better editor support and documentation
Real-world Application: Database Repository Pattern
Let's see a more comprehensive example using the repository pattern with dependency classes:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from typing import List, Dict, Any
app = FastAPI()
class Database:
def __call__(self) -> Session:
# In a real app, this would create a database session
return Session() # Simplified for example
class UserRepository:
def __init__(self, db_dependency=None):
# Use dependency injection to get the database dependency
self.db_dependency = db_dependency or Database()
def __call__(self) -> "UserRepository":
# This makes the class instance available for the route
# The database session is obtained when needed
return self
def get_db(self) -> Session:
# Get database session when needed
return self.db_dependency()
def get_user(self, user_id: int) -> Dict[str, Any]:
db = self.get_db()
# In a real app, this would query the database
# user = db.query(User).filter(User.id == user_id).first()
# Simplified example:
if user_id < 1:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}
def get_users(self, skip: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
db = self.get_db()
# In a real app, this would query the database
# users = db.query(User).offset(skip).limit(limit).all()
# Simplified example:
return [
{"id": i, "name": f"User {i}", "email": f"user{i}@example.com"}
for i in range(skip + 1, skip + limit + 1)
]
# Create an instance of the repository
user_repository = UserRepository()
@app.get("/users/{user_id}")
def read_user(user_id: int, repo: UserRepository = Depends(user_repository)):
return repo.get_user(user_id)
@app.get("/users/")
def read_users(skip: int = 0, limit: int = 10, repo: UserRepository = Depends(user_repository)):
return repo.get_users(skip, limit)
This example implements the repository pattern using dependency classes, providing a clean separation between database access and API logic.
Summary
Dependency classes in FastAPI provide a powerful way to organize complex dependency injection patterns in a structured, object-oriented manner. Key benefits include:
- State management and encapsulation
- Parameterized configuration
- Reusable components across routes
- Support for inheritance and composition
- Clean separation of concerns
By using dependency classes, you can build more maintainable and testable FastAPI applications, especially as they grow in complexity.
Additional Resources
- FastAPI official documentation on dependencies
- Advanced dependency patterns in FastAPI
- Design patterns in Python
Exercises
- Create a rate limiting dependency class that tracks request frequency by IP address
- Implement a caching dependency class that stores results of expensive operations
- Build a logging dependency class that records information about each request
- Develop a validation dependency class for complex input validation scenarios
- Create a feature flag dependency class that enables/disables features based on configuration
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)