Okay, here’s a comprehensive article on FastAPI Dependencies, designed for beginners but covering a lot of ground:
Learn FastAPI Dependencies: A Beginner’s Guide
FastAPI isn’t just about speed; it’s about maintainability, readability, and testability. A core feature that enables all three of these is its powerful dependency injection system. This system, often just referred to as “Dependencies” in FastAPI, might seem magical at first, but it’s built on simple, solid principles. This guide will break down the concept, show you how to use it, and explain why it’s so beneficial.
What is Dependency Injection?
At its heart, dependency injection (DI) is a design pattern. It’s a way of providing the things (dependencies) that a piece of code (like a function or a class) needs to do its job, without that code having to create those things itself. Think of it like this:
-
Without DI: You’re building a house. You need a hammer. You go to the hardware store, buy a hammer, and then use it. Your house-building code is directly responsible for getting the hammer.
-
With DI: You’re building a house. You tell your foreman, “I need a hammer.” The foreman (the DI system) hands you a hammer. You don’t care where it came from; you just have the tool you need.
In programming terms, “dependencies” can be anything:
- Database connections
- Configuration settings
- Authentication information (like user details)
- External API clients
- Reusable utility functions
- Shared objects or resources
Why Use Dependency Injection?
The benefits of DI are numerous, especially in larger applications:
-
Loose Coupling: Your code isn’t tightly bound to specific implementations of its dependencies. You can swap out one database connection for another, or one authentication method for another, without changing the core logic of your functions.
-
Testability: This is huge. When testing, you can easily “mock” or “stub” your dependencies. Instead of needing a real database connection, you can provide a fake one that returns predictable data. This makes your tests faster, more reliable, and isolated.
-
Reusability: Dependencies can be reused across multiple parts of your application. You define how to get a database connection once, and then inject it wherever it’s needed.
-
Readability: Your code becomes cleaner and more focused on its primary task. The “noise” of setting up dependencies is moved elsewhere.
-
Maintainability: Changes to dependencies are localized. If you need to update your database connection string, you only change it in one place (the dependency definition), not everywhere it’s used.
-
Organization: DI helps structure your application by clearly separating concerns. Your route handlers focus on handling requests, while dependencies handle providing resources.
FastAPI’s Dependency System: The Basics
FastAPI leverages Python’s type hints to make dependency injection incredibly intuitive. Here’s the fundamental pattern:
“`python
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db(): # This is our dependency
db = “Imagine this is a database connection” # Simplified for example
try:
yield db
finally:
print(“Closing database connection”) # Imagine closing the connection
@app.get(“/items/”)
async def read_items(db = Depends(get_db)): # Injecting the dependency
# Use the ‘db’ object here
return {“db”: db, “message”: “Items retrieved successfully!”}
“`
Let’s break this down:
get_db()
: This is a dependency function. It’s a regular Python function that returns the dependency (in this case, a string simulating a database connection). Crucially, it usesyield
instead ofreturn
. This makes it a generator function.yield
: Theyield
keyword is essential for dependencies that need setup and teardown (like opening and closing a database connection). The code beforeyield
runs when the dependency is requested. The code afteryield
(inside thefinally
block) runs after the route handler finishes, even if there’s an error. This ensures resources are always cleaned up properly.Depends(get_db)
: This is where the magic happens.Depends
is a class provided by FastAPI. You pass your dependency function (get_db
) to it. FastAPI will:- Call
get_db()
. - Execute the code up to the
yield
statement. - Get the value yielded by
get_db()
(our “database connection”). - Inject that value as the
db
parameter inread_items()
. - After
read_items()
completes (or if an error occurs), execute the code in thefinally
block ofget_db()
.
- Call
db = Depends(get_db)
: This is how you declare the dependency in your route handler. You use a type hint (db
) and assign it the result ofDepends(get_db)
. FastAPI uses the type hint and theDepends
call to figure out what to inject.
Key Concepts and Features
Now let’s dive deeper into the various aspects and capabilities of FastAPI’s dependency system:
1. Dependencies with Classes
You’re not limited to functions. You can also use classes as dependencies:
“`python
from fastapi import FastAPI, Depends
app = FastAPI()
class Database:
def init(self):
self.data = []
def add_item(self, item):
self.data.append(item)
def get_items(self):
return self.data
def get_database():
return Database() # Return an instance of the class
@app.post(“/items/”)
async def create_item(item: str, db: Database = Depends(get_database)):
db.add_item(item)
return {“message”: “Item created successfully!”}
@app.get(“/items/”)
async def read_items(db: Database = Depends(get_database)):
return {“items”: db.get_items()}
“`
Here, Database
is a class representing our (very simple) database. get_database()
returns an instance of this class. FastAPI will create a new instance of Database
for each request. This is important: dependencies are typically request-scoped by default, meaning a new instance is created for each incoming request.
2. Dependencies with Sub-Dependencies
Dependencies can depend on other dependencies! This is incredibly powerful for building complex, layered applications:
“`python
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
class Database:
def init(self, connection_string: str):
self.connection_string = connection_string
# Imagine connecting to a database here
def get_data(self):
return "Data from the database"
def get_connection_string():
return “your_database_connection_string”
def get_db(connection_string: str = Depends(get_connection_string)):
return Database(connection_string)
@app.get(“/data/”)
async def read_data(db: Database = Depends(get_db)):
return {“data”: db.get_data()}
“`
In this example:
get_connection_string()
is a simple dependency that provides the connection string.get_db()
is a dependency that depends onget_connection_string()
. It receives the connection string and uses it to create aDatabase
instance.read_data()
depends onget_db()
, which in turn depends onget_connection_string()
. FastAPI handles the entire chain automatically.
3. Dependencies in Path Operation Decorators
You can also define dependencies directly within the path operation decorators (@app.get
, @app.post
, etc.):
“`python
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
async def verify_token(x_token: str = Header(…)):
if x_token != “fake-super-secret-token”:
raise HTTPException(status_code=400, detail=”X-Token header invalid”)
async def verify_key(x_key: str = Header(…)):
if x_key != “fake-super-secret-key”:
raise HTTPException(status_code=400, detail=”X-Key header invalid”)
@app.get(“/items/”, dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{“item”: “Foo”}, {“item”: “Bar”}]
“`
Here:
verify_token
andverify_key
are dependencies that check for specific headers (X-Token
andX-Key
).- The
dependencies
parameter in@app.get
takes a list of dependencies. These dependencies will be executed before theread_items
function. - If any of these dependencies raise an exception (like
HTTPException
), the request processing will stop, and the exception will be handled by FastAPI (returning an appropriate error response).
This is very useful for things like authentication, authorization, or validating request data before it even reaches your route handler.
4. Global Dependencies
You can apply dependencies to all path operations in your application using the dependencies
parameter of the FastAPI
class itself:
“`python
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI(dependencies=[Depends(verify_token)]) # Global dependency
async def verify_token(x_token: str = Header(…)):
if x_token != “fake-super-secret-token”:
raise HTTPException(status_code=400, detail=”X-Token header invalid”)
return x_token # Important to return a value
@app.get(“/items/”)
async def read_items(valid_token = Depends(verify_token)):
return [{“item”: “Foo”}, {“item”: “Bar”}]
@app.get(“/users/”) # verify_token is applied here too!
async def read_users():
return [{“user”: “Alice”}, {“user”: “Bob”}]
“`
Now, verify_token
will be executed for every request to your application. Note that the read_items
endpoint also explicitly uses Depends(verify_token)
. This is allowed, but it’s somewhat redundant in this case because the global dependency already covers it. However, it’s important to understand that you can still access the return value of the dependency in your path operation function, even if it’s a global dependency. You need to re-declare it with Depends
to get the return value.
5. Dependencies with yield
and Context Managers
We’ve seen yield
used for setup and teardown. You can also use Python’s with
statement (context managers) within your dependency functions:
“`python
from fastapi import FastAPI, Depends
import sqlite3
app = FastAPI()
def get_db():
with sqlite3.connect(“mydatabase.db”) as db: # Context manager
yield db
@app.get(“/items/”)
async def read_items(db: sqlite3.Connection = Depends(get_db)):
cursor = db.cursor()
cursor.execute(“SELECT * FROM items”)
return cursor.fetchall()
“`
This is equivalent to using try...finally
, but it’s often cleaner and more readable, especially when dealing with resources that support context managers (like database connections, file handles, etc.).
6. Using Annotated
(FastAPI 0.95+ and Python 3.9+)
FastAPI 0.95 introduced Annotated
from Python’s typing
module, which provides a more explicit and powerful way to declare dependencies and metadata. While Depends()
still works perfectly, Annotated
can improve clarity, especially in complex scenarios.
“`python
from typing import Annotated
from fastapi import FastAPI, Depends, Query
app = FastAPI()
async def pagination_params(
skip: Annotated[int, Query(ge=0)] = 0, # >= 0
limit: Annotated[int, Query(ge=1)] = 100, # >= 1
):
return {“skip”: skip, “limit”: limit}
@app.get(“/items/”)
async def read_items(pagination: Annotated[dict, Depends(pagination_params)]):
return {
“message”: “Items retrieved with pagination”,
“pagination”: pagination,
}
“`
Key changes here:
Annotated[int, Query(ge=0)]
: Instead of justskip: int = 0
, we useAnnotated
. The first argument is the type (int
). The second argument is the metadata, in this case,Query(ge=0)
. This tells FastAPI thatskip
is a query parameter, it should be an integer, and it must be greater than or equal to 0.pagination: Annotated[dict, Depends(pagination_params)]
: Similarly, we useAnnotated
to clearly indicate thatpagination
is a dictionary and it’s obtained via thepagination_params
dependency.
Annotated
makes your intentions much clearer, and it’s the recommended approach for new code. It also allows for more complex metadata to be associated with your parameters.
7. Using Pydantic Models as Dependencies
You can use Pydantic models as dependencies, which is especially useful for validating request bodies:
“`python
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
class ItemUpdate(BaseModel): #separate model that only updates certain fields
name: str | None = None
description: str | None = None
price: float | None = None
def get_item_by_id(item_id: int) -> Item: #Simulates retrieving an item
# In a real application, you’d fetch this from a database
if item_id == 1:
return Item(name=”Foo”, description=”A foo item”, price=50.2)
return None
@app.put(“/items/{item_id}”)
async def update_item(
item_id: int,
item_update: ItemUpdate,
original_item: Item = Depends(get_item_by_id),
):
if original_item is None:
raise HTTPException(status_code=404, detail=”Item not found”)
# Update only the fields provided in item_update
update_data = item_update.dict(exclude_unset=True)
updated_item = original_item.copy(update=update_data)
#In a real app, you would persist 'updated_item' to your database
return updated_item
``
exclude_unset=True` is key, so that the returned dictionary only includes the values that were passed in the request, so you can update only certain fields.
Here you are utilizing a dependency to get an existing item, and another Pydantic model is used to take in potentially updated values.
8. Caching Dependencies
By default, FastAPI creates a new instance of a dependency for each request. However, you can cache dependencies using the use_cache
parameter of Depends
:
“`python
from fastapi import FastAPI, Depends
app = FastAPI()
counter = 0
def get_counter(reset: bool = False):
global counter
if reset:
counter = 0
counter += 1
return counter
@app.get(“/cached-counter/”)
async def cached_counter(count: int = Depends(get_counter, use_cache=True)): # Cached
return {“count”: count}
@app.get(“/uncached-counter/”)
async def uncached_counter(count: int = Depends(get_counter, use_cache=False)): # Not cached
return {“count”: count}
@app.get(“/reset-counter/”)
async def reset_counter(count: int = Depends(get_counter, use_cache=False)): # Not cached, to reset
return {“count”: get_counter(reset = True)}
“`
use_cache=True
: The result ofget_counter()
will be cached for the duration of a single request. Subsequent calls toDepends(get_counter)
within the same request will return the cached value. Different requests will still get different values.use_cache=False
: This explicitly disables caching, ensuring thatget_counter()
is called every time, even within the same request.
Caching is useful for dependencies that are expensive to create or that you want to share across multiple parts of a single request. Be careful when caching mutable objects; changes made in one part of the request will be reflected in others.
9. Dependency Overrides (for Testing)
One of the most powerful features of FastAPI’s dependency system is the ability to override dependencies during testing. This allows you to replace real dependencies with mock or stub implementations, making your tests fast, reliable, and independent of external resources.
“`python
app.py
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
return “Real database connection”
@app.get(“/items/”)
async def read_items(db = Depends(get_db)):
return {“db”: db}
test_app.py
from fastapi.testclient import TestClient
from .app import app, get_db # Import the app and the dependency
client = TestClient(app)
def get_mock_db():
return “Mock database connection”
app.dependency_overrides[get_db] = get_mock_db # Override the dependency
def test_read_items():
response = client.get(“/items/”)
assert response.status_code == 200
assert response.json() == {“db”: “Mock database connection”} #check mock db
Reset overrides
def test_read_items_reset():
app.dependency_overrides = {} #empty override dictionary
response = client.get(“/items/”)
assert response.status_code == 200
assert response.json() == {“db”: “Real database connection”} #check real db
“`
Here’s what’s happening in the test file:
TestClient
: We use FastAPI’sTestClient
to simulate HTTP requests to our application.get_mock_db()
: This is our mock dependency. It returns a simple string instead of a real database connection.app.dependency_overrides[get_db] = get_mock_db
: This is the key line. We’re modifying theapp.dependency_overrides
dictionary. We’re saying, “Wheneverget_db
is requested as a dependency, useget_mock_db
instead.”test_read_items()
: Our test function now uses theTestClient
to make a request to/items/
. Because we’ve overridden the dependency,read_items
will receive the “Mock database connection” instead of the real one.test_read_items_reset()
: It is crucial to remember to reset your overrides if you have multiple tests, or else they may interfere with each other. The easiest way to do this is to set theapp.dependency_overrides
dictionary back to an empty dictionary{}
.
This ability to override dependencies is essential for writing effective unit tests for your FastAPI applications. You can test your route handlers in isolation, without needing to worry about external services or databases.
Best Practices
- Keep Dependencies Small and Focused: Each dependency should have a single, well-defined responsibility.
- Use
yield
for Setup and Teardown: Always useyield
(or context managers) for dependencies that need to manage resources. - Leverage Type Hints: FastAPI relies heavily on type hints for dependency injection. Use them consistently.
- Use
Annotated
for Clarity (FastAPI 0.95+): PreferAnnotated
over plainDepends()
for better readability and more expressive metadata. - Override Dependencies for Testing: This is crucial for writing good unit tests.
- Consider Caching: Use
use_cache=True
for expensive dependencies that can be safely shared within a single request. - Separate Models for PUT/PATCH: When updating resources, use a separate Pydantic model that allows partial updates.
Conclusion
FastAPI’s dependency injection system is a powerful tool that promotes clean, maintainable, and testable code. By understanding the core concepts and best practices, you can build robust and scalable applications with ease. This guide has covered a lot of ground, from the basics to advanced techniques. Experiment with these concepts, and you’ll quickly see how dependencies can transform your FastAPI development workflow. The key takeaway is to embrace the pattern of letting FastAPI provide the things your code needs, rather than having your code be responsible for creating them. This simple shift in thinking leads to significant improvements in code quality and maintainability.