Handbooks / FastAPI / Chapter 2
Dependency Injection
7 pages · ~5 min✓ Reviewed
Builds on Routing and Pydantic Models. Next up: Async and Databases.
Depends and the Mental Model
1. What Depends Does
- A param
= Depends(fn)is resolved before the handler runs - The callable's return value is passed in as the argument
- Any callable works:
def,async def, or a class - Its own params are parsed like path function params: path, query, header, body
- Requestarrives
- Call depsFastAPI does it
- Handlergets the values
- Responsesent back
2. Sync vs Async Deps
| Dependency | Runs on | Catch |
|---|---|---|
def | threadpool | blocking is fine |
async def | event loop | a blocking call stalls the loop |
| Mixed | each in its own place | mixing is fine |
3. Example and Mistakes
def common(q: str | None = None, limit: int = 10): return {'q': q, 'limit': limit} @app.get('/items') def items(p: dict = Depends(common)): return p
q and limit are read as query params of /items
Depends(get_db())calls it at import time- It injects one fixed value, not a fresh one per request
- Correct:
Depends(get_db), the function uncalled
Write Depends(get_db), never Depends(get_db()). FastAPI does the calling.
Dependency Trees and Caching
1. Trees and the Typical Chain
- A dependency can declare its own Depends(...) params, which forms a tree
- FastAPI resolves the tree bottom-up: leaves first, then the parents
- The endpoint declares only require_admin; the rest is pulled in
- 1get_dbleaf
- 2get_current_usertoken + db
- 3require_adminneeds user
- 4endpointhandler runs
2. Per-Request Cache
| Default | use_cache=False | |
|---|---|---|
| Same dep declared twice | Runs once, result shared | Runs each time declared |
| Scope | One request | One request |
fresh: int = Depends(counter, use_cache=False)forces a new call at this declaration
3. Annotated Alias
Yield Dependencies: Setup and Teardown
1. Setup, Value, Teardown
- Code before
yieldis setup - The yielded value is injected into the handler
- Code after
yieldis teardown, run once the response is sent finallyguarantees cleanup even when the handler raises
def get_db(): db = Session() # setup try: yield db # injected finally: db.close() # teardown
standard pattern
2. Teardown Order
- 1a upsetup
- 2b upsetup
- 3b downteardown first
- 4a downteardown last
3. Errors and Sync vs Async
- Handler exceptions are re-raised at the yield
exceptthere to rollback, thenraise- Cannot change the response from teardown: it already started streaming
Class Dependencies and Global Dependencies
1. Class Dependencies
- A class is callable:
Depends(Pagination)callsPagination(...) __init__params are read as query/path paramsDepends()with no argument uses the type annotation as the callable- Wins over a dict for typed attributes (
p.skip) and editor autocomplete
class Pagination: def __init__(self, skip: int = 0, limit: int = 10): self.skip, self.limit = skip, limit @app.get('/items') def items(p: Pagination = Depends()): return p.skip, p.limit
Depends() reads the annotation: Pagination
2. Global Dependencies
| Level | Syntax | Covers |
|---|---|---|
| Route | @app.get('/x', dependencies=[Depends(verify_key)]) | that route |
| Router | APIRouter(dependencies=[Depends(auth)]) | every route in it |
| App |
Overriding Dependencies in Tests
1. The Override Map
- app.dependency_overrides[orig] = fake replaces it for every request
- Key is the original function object, not a string
- Fake's own params resolve as usual:
lambda: User(id=1)skips auth
app.dependency_overrides[get_db] = lambda: test_session client = TestClient(app) client.get('/items') # handler gets test_session
override first, then call
2. What Matches
| In Depends(...) | Override hits? |
|---|---|
| Same function object | Yes |
Annotated alias (wraps same function) | Yes |
| Imported copy or wrapper | No |
3. Reset Every Test
@pytest.fixture def client(): app.dependency_overrides[get_db] = lambda: test_session yield TestClient(app) app.dependency_overrides.clear()
teardown after yield clears it
Dependency Cheat Sheet
1. Declare and Class
def items( db: Annotated[Session, Depends(get_db)], p: Annotated[Pagination, Depends()], ): return db.query(Item).offset(p.skip)
function and class dependencies
- Pass fn uncalled:
Depends(get_db) use_cache=Falsedisables per-request reuse- Class dep:
Depends(Cls)or bareDepends(); readp.skip
2. Yield Dependency
- 1Setupbefore yield
- 2yield valueinjected
- 3Response senthandler done
- 4finallyteardown, reverse order
3. Scope Ladder
| Level | Syntax | Covers |
|---|---|---|
| Route | dependencies=[...] | one route |
| Router | APIRouter(dependencies=[...]) | its routes |
Check yourself
get_db is used by both user and admin, and the route declares only admin. How many times does get_db run per request?
- Once: the same callable in one tree is cached per request
use_cache=Falseon a declaration forces a second call
The route needs b, and b uses Depends(a). Both a and b use yield. In what order do their setup, handler and teardown run?
- a up, b up, handler, b down, a down
- Teardown runs in reverse order of setup, after the response
A test sets app.dependency_overrides['get_db'] = fake, yet the real database is still hit. Why?
- The key must be the function object
get_db, not a string - The string never matches, so the override is silently ignored