FastAPI’s dependency injection system is actually a powerful form of explicit, runtime-checked inversion of control, not just a convenience for passing parameters.

Let’s see it in action. Imagine you have a database connection you want to reuse across multiple endpoints.

from fastapi import FastAPI, Depends

# Simulate a database connection
class Database:
    def __init__(self):
        print("Connecting to database...")
        self.conn = "db_connection_object" # Placeholder for actual connection

    def get_data(self, item_id: int):
        print(f"Fetching data for item {item_id} using {self.conn}")
        return {"id": item_id, "name": "Sample Item"}

app = FastAPI()

# This function will be called to provide the database instance
def get_db():
    db = Database()
    try:
        yield db
    finally:
        print("Closing database connection...")
        # In a real app, you'd close the connection here

@app.get("/items/{item_id}")
def read_item(item_id: int, db: Database = Depends(get_db)):
    return db.get_data(item_id)

@app.get("/users/{user_id}")
def read_user(user_id: int, db: Database = Depends(get_db)):
    # Imagine fetching user data which also needs the DB
    print(f"Fetching user {user_id} using {db.conn}")
    return {"user_id": user_id, "username": "test_user"}

When you run this and hit /items/5, you’ll see:

Connecting to database...
Fetching data for item 5 using db_connection_object
Closing database connection...

And for /users/10:

Connecting to database...
Fetching user 10 using db_connection_object
Closing database connection...

Notice how get_db is called each time an endpoint that depends on it is hit. The yield keyword is crucial here; it’s what makes get_db a generator and allows FastAPI to manage setup and teardown. The db: Database = Depends(get_db) in the endpoint function is the magic part. FastAPI sees Depends(get_db), knows it needs to call get_db, and injects whatever get_db yields into the db parameter. If get_db raises an error, the request fails before your endpoint logic runs. If there’s a finally block in get_db, it’s guaranteed to run after your endpoint finishes, whether it succeeds or fails.

This system solves the problem of managing shared resources like database connections, authentication handlers, or configuration settings in a clean, testable, and robust way. Instead of scattering connection logic everywhere, you centralize it in dependency functions. These functions can have their own dependencies, creating a tree of dependencies that FastAPI resolves automatically. The type hints (db: Database) are used by FastAPI for validation and documentation, but the core mechanism is the Depends object.

The real power comes when you start nesting dependencies or using Depends with callables that aren’t just generators. For instance, Depends can take a dict, a list, or even another Depends call. This allows you to build complex dependency graphs. A common pattern is to have a dependency that itself depends on another dependency, like an authentication dependency that depends on a user retrieval dependency.

A key aspect often overlooked is how FastAPI handles dependency scope. By default, dependencies are re-evaluated for every request. However, you can achieve request-scoped (default), session-scoped, or even application-scoped dependencies using custom dependency overrides or by structuring your code appropriately, especially when integrating with libraries that manage their own scopes. This is critical for performance and resource management; you don’t want to create a new database connection for every single sub-operation within a single request if it can be avoided.

The next logical step is understanding how to use dependency overrides for testing, allowing you to mock complex dependencies without altering your main application code.

Want structured learning?

Take the full Fastapi course →