Chapter 10 · Dependency Injection Basics
Chapter goals: Understand what dependency injection solves, learn to declare dependencies with
Depends, reuse dependencies withAnnotatedtype aliases, and master sync/async dependency mixing and request-level caching behavior.
10.1 What Dependency Injection Solves
"Dependency Injection" (DI) means: the path operation function only declares what it needs, and the framework is responsible for constructing and injecting it. Official listed use cases:
- Shared logic across multiple endpoints (pagination parsing, common query processing);
- Shared database connections/sessions;
- Enforcing security, authentication, and role checks.
Without DI, every endpoint duplicates the same code; with DI, write the logic once and FastAPI auto-calls and injects the result on each request.
10.2 First Dependency: Depends
A dependency (a "dependable") is a plain function whose parameters are structurally identical to the path operation function's:
from typing import Annotated
from fastapi import FastAPI, Depends
app = FastAPI()
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
# Dependency function: declares q/skip/limit as query params and packs them into a dict
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def list_items(
commons: Annotated[dict, Depends(common_parameters)], # Declare the dependency
):
return {"items": ["a", "b"], **commons}
@app.get("/users/")
async def list_users(
commons: Annotated[dict, Depends(common_parameters)], # Same dependency reused
):
return {"users": ["u1"], **commons}Key rule: pass the function itself to Depends() — do not add parentheses (write Depends(common_parameters) not Depends(common_parameters())).
On each request, FastAPI:
- Calls
common_parameters()with the correct parameters (whereq/skip/limitare parsed and validated as query params); - Assigns the return value to the path function's
commonsparameter.
Note: params declared in the dependency (q/skip/limit) automatically appear in the OpenAPI docs — you can see and debug them in /docs.
10.3 Reusing with Annotated Type Aliases
Both endpoints above write Annotated[dict, Depends(common_parameters)] — still repetitive. Since Annotated is pure Python standard syntax, store it as a type alias variable:
from typing import Annotated
from fastapi import FastAPI, Depends
Commons = Annotated[dict, Depends(common_parameters)] # Type alias, not a FastAPI feature
app = FastAPI()
@app.get("/items/")
async def list_items(commons: Commons): # One line
return {"items": ["a"], **commons}
@app.get("/users/")
async def list_users(commons: Commons):
return {"users": ["u1"], **commons}The official recommendation from 0.95.0 onward. Compared to the old default-value style (commons: dict = Depends(common_parameters)), Annotated's advantage: type information is fully preserved — editor autocomplete, mypy static checking all work correctly, and alias reuse in large codebases is extremely convenient.
10.4 Mixing Sync and Async Dependencies Freely
Dependencies, like path functions, support both async def and plain def, and they mix arbitrarily — an async def path function can use a sync dependency and vice versa; FastAPI auto-selects the correct event loop/thread pool execution path.
Selection principle (consistent with Chapter 16):
- Dependency internally does I/O with an async client (e.g.,
AsyncSession) →async def; - Dependency is pure computation or calls a synchronous blocking library (e.g., legacy
psycopg2) → plaindef(avoid blocking the event loop).
import time
from fastapi import Depends, FastAPI
app = FastAPI()
def sync_dep() -> dict:
time.sleep(0.01) # Simulate a synchronous blocking call; FastAPI dispatches to thread pool
return {"source": "sync"}
async def async_dep() -> dict:
await asyncio.sleep(0.01) # Non-blocking async I/O
return {"source": "async"}10.5 Request-Level Caching
By default, a dependency is re-executed on every path function call that depends on it — even within the same request. To cache a dependency to execute only once per request:
from fastapi import Depends, FastAPI
app = FastAPI()
_call_count = 0
def counting_dep() -> dict:
global _call_count
_call_count += 1
return {"call_number": _call_count}
@app.get("/items/")
async def get_items(dep: dict = Depends(counting_dep)):
return dep
@app.get("/items/{item_id}")
async def get_item(item_id: int, dep: dict = Depends(counting_dep)):
return {"item_id": item_id, **dep}Visiting /items/ then /items/1 in the same request: counting_dep runs twice (two path functions). But if two dependencies in the same function both depend on the same sub-dependency, that sub-dependency runs only once — request-level caching by identity.
10.6 Chapter Summary
- DI solves code duplication: declare what you need, framework constructs and injects;
Depends(func)passes the function itself, not a call;Annotated[Type, Depends(...)]type aliases eliminate repetition;- Sync and async dependencies mix freely; FastAPI handles execution context;
- Dependencies are cached per-request by identity — shared sub-dependencies run once.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What does Depends() do when passed a function?
2. Why use Annotated[type, Depends(...)] instead of param: type = Depends(...)?
3. If two path functions in the same request both depend on the same sub-dependency, how many times does the sub-dependency execute?
4. When should you use async def vs plain def for a dependency?
🛠️ Hands-On Exercises
- Write a dependency
get_pagination(skip: int = 0, limit: int = 10)returning{"skip": skip, "limit": limit}; use it in two endpoints viaAnnotatedalias. - Create a
current_userdependency that reads a Cookie and returns a user dict; use it in three endpoints; verify it runs only once per request. - Mix a sync dependency (plain def with time.sleep) and an async dependency (async def with asyncio.sleep) in the same app; verify both work correctly.
Proceed to Chapter 11: Advanced Dependency Injection.