FastAPI’s dependency injection system is so powerful because it lets you treat your application’s configuration and dependencies as first-class citizens, rather than as hidden global state.
Let’s see it in action. Imagine a simple FastAPI application that needs to fetch user data.
from fastapi import FastAPI, Depends
app = FastAPI()
# A simple database connection object (simulated)
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
print(f"Connecting to: {self.connection_string}")
def get_user(self, user_id: int):
return {"user_id": user_id, "name": "Alice"}
# A function that returns a database instance
def get_db():
# In a real app, this would be more complex, reading from env vars etc.
return Database(connection_string="postgresql://user:password@host:port/dbname")
@app.get("/users/{user_id}")
async def read_user(user_id: int, db: Database = Depends(get_db)):
user_data = db.get_user(user_id)
return user_data
# To run this:
# 1. Save as main.py
# 2. Install uvicorn: pip install uvicorn fastapi
# 3. Run: uvicorn main:app --reload
# 4. Visit: http://127.0.0.1:8000/users/123
When you visit http://127.0.0.1:8000/users/123, FastAPI doesn’t just magically know how to get a Database object. It sees db: Database = Depends(get_db) and knows it needs to call the get_db function. The return value of get_db is then passed as the db argument to your read_user endpoint. You’ll see "Connecting to: postgresql://user:password@host:port/dbname" printed in your console the first time the get_db function is executed.
The magic behind Depends is how it handles the lifecycle and resolution of these dependencies. FastAPI builds a dependency graph for each request. When a dependency is requested, FastAPI checks if it has already been created for that request. If so, it reuses the existing instance. If not, it calls the dependency provider function (like get_db) and stores the result for future use within that same request. This ensures that, for instance, a database connection is established only once per request, not for every single dependency that might need it.
This mechanism allows for incredible flexibility. You can:
- Create singletons: By default, dependencies are scoped to the request. For true application-wide singletons, you can use
singleton=TrueinDepends, though FastAPI’s standard usage often achieves this implicitly for certain types of objects if they are constructed outside of request-specific logic. - Handle authentication: You can have a dependency that checks a token and returns the authenticated user object, or raises an HTTPException if authentication fails.
- Manage configuration: Load settings from environment variables or a config file within a dependency provider.
- Mock dependencies for testing: In your tests, you can simply provide a mock object instead of the real dependency.
The core idea is that Depends acts as a declaration of what you need, and the function passed to Depends is the how to get it. This separation is key. It means your endpoint functions (read_user in this case) are clean, focused on their primary logic, and oblivious to the intricate details of setting up their prerequisites.
Consider how you might handle different database configurations based on an environment variable.
import os
from fastapi import FastAPI, Depends
app = FastAPI()
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
print(f"Connecting to: {self.connection_string}")
def get_user(self, user_id: int):
return {"user_id": user_id, "name": "Alice"}
def get_db_connection_string() -> str:
env = os.environ.get("APP_ENV", "development")
if env == "production":
return "postgresql://prod_user:prod_pass@prod_host:5432/proddb"
else:
return "postgresql://dev_user:dev_pass@localhost:5432/devdb"
def get_db(connection_string: str = Depends(get_db_connection_string)) -> Database:
return Database(connection_string=connection_string)
@app.get("/users/{user_id}")
async def read_user(user_id: int, db: Database = Depends(get_db)):
user_data = db.get_user(user_id)
return user_data
Here, get_db now depends on get_db_connection_string. FastAPI will first execute get_db_connection_string, get its result, and then pass that result to get_db. This creates a chain of dependencies, all managed automatically. If you set APP_ENV=production and run the app, you’ll see the production connection string being used.
The most surprising aspect for many is how easily this scales to complex scenarios without becoming unmanageable. You can have dependencies that themselves depend on other dependencies, creating a tree of resolution. FastAPI’s internal caching per request means that even with deep dependency chains, computation is only performed once per unique dependency within a single request’s lifecycle. This makes it highly performant, as you’re not re-instantiating database connections or re-authenticating users on every single micro-operation within a request.
The next logical step is to explore how to override these dependencies, especially for testing purposes or for different operational environments.