Dependency Injection
Understand the power of dependency injection in FastAPI for managing dependencies in your applications.
Content
Dependency Injection System
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Dependency Injection System — FastAPI's backstage orchestra
You already learned how to create dependencies and how to use them. Now we rip off the curtain and look at the whole system: how FastAPI wires everything together, what it caches, how it orders things, and how to bend it when you need to (tests, teardown, security, etc.). Think of this as the conductor explaining how the orchestra actually plays the symphony.
Why this matters (quick recap + forward motion)
You know how to write a dependency function and put Depends(...) in a route. Great. But when your app grows, mysterious bugs crop up: duplicate DB connections, teardown never runs, slow endpoints because resources re-run, or tests that need to swap out components. The Dependency Injection System is what decides: which functions get called, in what order, whether results are reused, and how cleanup happens. Master this and you move from "it works" to "it scales and stays sane." Also — remember Request & Response Handling? The DI system is what hands those Request/Response objects to your dependencies so middleware, security, and response mutation all play nice.
The big picture: how FastAPI resolves dependencies
- FastAPI inspects the signature of a path operation function.
- For parameters with a default value of Depends(...), FastAPI marks them as dependencies.
- It builds a dependency graph (a directed acyclic graph) that includes sub-dependencies.
- FastAPI resolves the graph topologically — dependencies before dependents.
- By default, results are cached per-request so the same dependency callable is executed once per request unless told otherwise.
- If a dependency is a generator (uses
yield), FastAPI treats it as a context manager: code afteryieldruns as teardown after the response is returned.
Quick metaphor: dependencies are actors in a play. FastAPI directs the rehearsal order, gives each actor their lines once per show (request), and calls "curtain" — running any teardown — after the performance.
Important behaviors and features (with examples)
1) Caching (per-request reuse)
By default, if two parameters depend on the same dependency callable, FastAPI calls it only once and reuses the returned value.
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users")
def read_users(db=Depends(get_db)):
return db.query(User).all()
@app.get("/posts")
def read_posts(db=Depends(get_db)):
return db.query(Post).all()
Both routes, in the same request, will share the same get_db() result when used inside a single path operation with multiple dependencies referencing it. If you need fresh execution every time (rare), you can disable caching using Depends(..., use_cache=False).
2) Yield dependencies — setup and teardown
Generators let you ensure resource cleanup (DB sessions, locks, temporary files):
async def lifespan_db():
db = create_db()
try:
yield db
finally:
await db.disconnect()
Code after yield will run after the response is sent. This is ideal for commits, closing sessions, releasing locks.
3) Sub-dependencies and ordering
Dependencies can depend on other dependencies. FastAPI flattens the graph and runs them in correct order (sub-dependencies first) and injects results into their parents.
- If A depends on B and C, B and C are resolved before A.
- Circular dependencies produce errors — avoid dependency loops.
4) Sync vs Async — transparent
Your dependency can be sync or async; FastAPI will run it in the appropriate context (async directly, sync in a threadpool for async endpoints). You don't need to write extra glue.
5) Automatic injection of Request/Response and special types
If you type-annotate a parameter as Request or Response (from fastapi), FastAPI will inject them automatically — no Depends required. Use this to mutate responses from inside dependencies (e.g., add headers):
from fastapi import Request, Response
def add_request_id(response: Response):
response.headers["x-request-id"] = str(uuid4())
@app.get("/items")
def read_items(response: Response = Depends(add_request_id)):
return {"ok": True}
6) Class-based dependencies
Pass objects with state by implementing call:
class Auth:
def __init__(self, cred_store):
self.cred_store = cred_store
def __call__(self, token: str = Depends(oauth2_scheme)):
return self.cred_store.get_user(token)
This is useful for configuration injected once (when router/app created) and used per-request.
Testing and overrides
A major superpower: override dependencies in tests to control environment and remove external IO.
def fake_get_db():
db = TestingDB()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = fake_get_db
client = TestClient(app)
You can override per-dependency or clear all at teardown. This makes unit tests deterministic and fast.
Pitfalls & gotchas (read this or suffer)
- Circular dependencies: The DI system can't resolve cycles — your design needs rethinking.
- Teardown timing:
yieldcleanup happens after response body is sent; be careful if you rely on teardown to complete before sending something else. - Caching surprises: If you mutate a cached object in a dependency, other dependents will see the mutation. Immutable returns are safer.
- Expensive sync dependencies in async endpoints: Sync functions run in a threadpool which is fine, but heavy CPU-bound work will block workers. Move computation to background tasks or separate services.
Handy reference table
| Feature | Default behavior | Override / notes |
|---|---|---|
| Caching | Per-request, use_cache=True | Use Depends(..., use_cache=False) to disable |
| Lifecycle | yield dependency: teardown after response |
Good for DB session commit/close |
| Resolution order | Topologically sorted (sub-deps first) | Circular => error |
| Type injection | Request, Response, BackgroundTasks auto-injected | No Depends needed |
| Testing | Override with app.dependency_overrides | Very powerful for mocks |
Quick mental model (do not panic)
- Dependencies are pure callables the system wires up.
- FastAPI builds a dependency graph and resolves it per-request.
- Values are cached per-request unless explicitly disabled.
- Generators = setup (before yield) + teardown (after response).
- You can replace, override, and compose dependencies reliably for testing and modular design.
Closing — a tiny checklist to graduate to DI maestro
- Use small, single-purpose dependencies (testable).
- Prefer
yieldfor resources that absolutely must be cleaned up. - Leverage caching to avoid duplicate work; disable when necessary.
- Use dependency_overrides in tests to remove external I/O.
- Avoid circular dependency graphs.
Final thought: FastAPI's DI system is both forgiving and powerful — it lets you compose tiny, testable units and stitch them into robust request handling. Once you get comfortable with resolution order, caching, and teardown, you'll stop wrestling with spaghetti and start orchestrating symphonies.
Summary: This topic connected creating and using dependencies to the overall system behavior. Next time: we'll explore how to use dependencies for Authorization patterns (OAuth2 flows and scopes) and for scaling resources with background tasks and sub-apps. Keep snacking while you code.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!