Learn FastAPI Dependencies: Beginner’s Guide

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:

  1. 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.

  2. 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.

  3. 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.

  4. Readability: Your code becomes cleaner and more focused on its primary task. The “noise” of setting up dependencies is moved elsewhere.

  5. 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.

  6. 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 uses yield instead of return. This makes it a generator function.
  • yield: The yield keyword is essential for dependencies that need setup and teardown (like opening and closing a database connection). The code before yield runs when the dependency is requested. The code after yield (inside the finally 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:
    1. Call get_db().
    2. Execute the code up to the yield statement.
    3. Get the value yielded by get_db() (our “database connection”).
    4. Inject that value as the db parameter in read_items().
    5. After read_items() completes (or if an error occurs), execute the code in the finally block of get_db().
  • 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 of Depends(get_db). FastAPI uses the type hint and the Depends 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 on get_connection_string(). It receives the connection string and uses it to create a Database instance.
  • read_data() depends on get_db(), which in turn depends on get_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 and verify_key are dependencies that check for specific headers (X-Token and X-Key).
  • The dependencies parameter in @app.get takes a list of dependencies. These dependencies will be executed before the read_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 just skip: int = 0, we use Annotated. The first argument is the type (int). The second argument is the metadata, in this case, Query(ge=0). This tells FastAPI that skip 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 use Annotated to clearly indicate that pagination is a dictionary and it’s obtained via the pagination_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

``
Here you are utilizing a dependency to get an existing item, and another Pydantic model is used to take in potentially updated values.
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.

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 of get_counter() will be cached for the duration of a single request. Subsequent calls to Depends(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 that get_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:

  1. TestClient: We use FastAPI’s TestClient to simulate HTTP requests to our application.
  2. get_mock_db(): This is our mock dependency. It returns a simple string instead of a real database connection.
  3. app.dependency_overrides[get_db] = get_mock_db: This is the key line. We’re modifying the app.dependency_overrides dictionary. We’re saying, “Whenever get_db is requested as a dependency, use get_mock_db instead.”
  4. test_read_items(): Our test function now uses the TestClient 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.
  5. 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 the app.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 use yield (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+): Prefer Annotated over plain Depends() 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top