FastAPI Dependency Injection with Depends: Annotated, Yield and Overrides (2026)
Almost every FastAPI app needs the same building blocks on many routes: query filters, the current user, a database session, feature flags. Copying that setup into each path operation gets messy fast. Dependency injection with Depends is FastAPI's answer: you write a small callable once, declare it on the route (or on another dependency), and FastAPI calls it, validates its parameters, caches the result for the request and injects the return value. In this tutorial we share query parameters across routes with the modern Annotated[..., Depends(...)] style, chain sub-dependencies for auth, open and close a fake database with yield, and swap a settings dependency in tests with app.dependency_overrides. Everything was tested on Python 3.13.5 with FastAPI 0.162.0, Starlette 1.10.0, Pydantic 2.14.3 and httpx 0.28.1 (used by TestClient), and every output block below is the real output of the scripts shown.
Why Depends in 2026?
- One place for shared logic: pagination, auth headers and DB sessions live in reusable callables instead of copy-pasted route code.
- Request-scoped cache: the same dependency declared twice in one request runs once; sub-dependencies are solved automatically.
- Cleanup with yield: code after
yieldruns after the response path, so sessions and connections close even when the handler raises. - Testable:
app.dependency_overridesreplaces production callables without starting a real database or external API. - Annotated style:
Commons = Annotated[dict, Depends(common_parameters)]keeps signatures readable and works with type checkers.
Install
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi==0.162.0 httpx
# TestClient ships with FastAPI / Starlette; httpx is its HTTP transport
We drive every example with fastapi.testclient.TestClient, so you do not need a running server. The same dependencies work unchanged under uvicorn in production.
Step 1: Share query parameters with Annotated and Depends
"""Step 1: shared query params via Depends + Annotated."""
import sys
from typing import Annotated
from fastapi import Depends, FastAPI, Query
from fastapi.testclient import TestClient
import fastapi
print("Python", sys.version.split()[0])
print("fastapi", fastapi.__version__)
app = FastAPI(title="DI commons demo")
def common_parameters(
q: Annotated[str | None, Query(max_length=50)] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
) -> dict:
return {"q": q, "skip": skip, "limit": limit}
Commons = Annotated[dict, Depends(common_parameters)]
@app.get("/items/")
def list_items(commons: Commons) -> dict:
items = [f"item-{i}" for i in range(commons["skip"], commons["skip"] + commons["limit"])]
if commons["q"]:
items = [x for x in items if commons["q"] in x]
return {"params": commons, "items": items, "count": len(items)}
@app.get("/users/")
def list_users(commons: Commons) -> dict:
return {"params": commons, "users": [f"user-{i}" for i in range(commons["limit"])]}
client = TestClient(app)
r1 = client.get("/items/", params={"skip": 2, "limit": 3})
r2 = client.get("/items/", params={"q": "item-2", "skip": 0, "limit": 5})
r3 = client.get("/users/", params={"limit": 2})
print("status_items", r1.status_code, r1.json())
print("status_filter", r2.status_code, r2.json())
print("status_users", r3.status_code, r3.json())
$ python di_commons.py
Python 3.13.5
fastapi 0.162.0
status_items 200 {'params': {'q': None, 'skip': 2, 'limit': 3}, 'items': ['item-2', 'item-3', 'item-4'], 'count': 3}
status_filter 200 {'params': {'q': 'item-2', 'skip': 0, 'limit': 5}, 'items': ['item-2'], 'count': 1}
status_users 200 {'params': {'q': None, 'skip': 0, 'limit': 2}, 'users': ['user-0', 'user-1']}
common_parameters is a normal function whose parameters FastAPI treats like route parameters: Query constraints still apply. We alias Annotated[dict, Depends(common_parameters)] as Commons and reuse it on /items/ and /users/. The first call returns items 2 to 4, the second filters to item-2, and the users route gets the same pagination shape without duplicating the query logic.
Step 2: Sub-dependencies and the per-request cache
"""Step 2: sub-dependencies and per-request cache."""
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
CALLS: list[str] = []
def get_token(authorization: Annotated[str | None, Header()] = None) -> str:
CALLS.append("get_token")
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="missing bearer token")
return authorization.removeprefix("Bearer ").strip()
def get_current_user(token: Annotated[str, Depends(get_token)]) -> dict:
CALLS.append("get_current_user")
if token != "secret-2026":
raise HTTPException(status_code=403, detail="invalid token")
return {"id": 42, "name": "Amir", "role": "editor"}
def get_editor(user: Annotated[dict, Depends(get_current_user)]) -> dict:
CALLS.append("get_editor")
if user["role"] != "editor":
raise HTTPException(status_code=403, detail="editors only")
return user
@app.get("/me")
def read_me(
user: Annotated[dict, Depends(get_current_user)],
# same get_token sub-dependency is cached once per request
again: Annotated[dict, Depends(get_current_user)],
) -> dict:
return {"user": user, "same_object": user is again, "calls": list(CALLS)}
@app.get("/editor-only")
def editor_page(editor: Annotated[dict, Depends(get_editor)]) -> dict:
return {"ok": True, "editor": editor["name"], "calls": list(CALLS)}
client = TestClient(app)
CALLS.clear()
bad = client.get("/me")
print("no_auth_status", bad.status_code, bad.json())
CALLS.clear()
ok = client.get("/me", headers={"Authorization": "Bearer secret-2026"})
print("ok_status", ok.status_code)
print("ok_body", ok.json())
CALLS.clear()
ed = client.get("/editor-only", headers={"Authorization": "Bearer secret-2026"})
print("editor_status", ed.status_code, ed.json())
$ python di_subdeps.py
no_auth_status 401 {'detail': 'missing bearer token'}
ok_status 200
ok_body {'user': {'id': 42, 'name': 'Amir', 'role': 'editor'}, 'same_object': True, 'calls': ['get_token', 'get_current_user']}
editor_status 200 {'ok': True, 'editor': 'Amir', 'calls': ['get_token', 'get_current_user', 'get_editor']}
Without an Authorization header the token dependency raises HTTPException 401 and the handler never runs. With a valid bearer token, /me declares get_current_user twice, yet the call log shows get_token and get_current_user only once: FastAPI caches the dependency result for the rest of the request, and same_object is True. The editor route adds one more layer; invalid tokens still stop at the first dependency that fails.
Step 3: Dependencies with yield for cleanup
"""Step 3: dependencies with yield (fake DB session cleanup)."""
from typing import Annotated, Iterator
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
LOG: list[str] = []
class FakeDB:
def __init__(self) -> None:
self.closed = False
self.rows = {1: {"id": 1, "title": "Hello Depends"}, 2: {"id": 2, "title": "Yield cleanup"}}
def get(self, item_id: int) -> dict | None:
return self.rows.get(item_id)
def close(self) -> None:
self.closed = True
def get_db() -> Iterator[FakeDB]:
db = FakeDB()
LOG.append("open")
try:
yield db
finally:
db.close()
LOG.append("close")
@app.get("/posts/{item_id}")
def read_post(item_id: int, db: Annotated[FakeDB, Depends(get_db)]) -> dict:
LOG.append(f"handler:{item_id}")
row = db.get(item_id)
if row is None:
raise HTTPException(status_code=404, detail="not found")
return {"post": row, "db_closed_during_handler": db.closed}
client = TestClient(app)
LOG.clear()
r = client.get("/posts/1")
print("ok_status", r.status_code, r.json())
print("lifecycle", LOG)
LOG.clear()
missing = client.get("/posts/99")
print("missing_status", missing.status_code, missing.json())
print("lifecycle_missing", LOG)
$ python di_yield.py
ok_status 200 {'post': {'id': 1, 'title': 'Hello Depends'}, 'db_closed_during_handler': False}
lifecycle ['open', 'handler:1', 'close']
missing_status 404 {'detail': 'not found'}
lifecycle_missing ['open', 'handler:99', 'close']
Everything before yield runs before the path operation; everything after runs afterwards (including on errors). The lifecycle log is always open → handler → close, and db_closed_during_handler is False while the handler runs. A missing post returns 404 and still closes the session. In a real app this is where you yield a SQLAlchemy Session and call session.close() in finally.
Step 4: Override dependencies in tests
"""Step 4: dependency_overrides for tests."""
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
def get_settings() -> dict:
return {"env": "prod", "feature_x": False}
@app.get("/feature")
def feature_flag(settings: Annotated[dict, Depends(get_settings)]) -> dict:
return {"env": settings["env"], "feature_x": settings["feature_x"]}
client = TestClient(app)
prod = client.get("/feature")
print("prod", prod.json())
def fake_settings() -> dict:
return {"env": "test", "feature_x": True}
app.dependency_overrides[get_settings] = fake_settings
test = client.get("/feature")
print("override", test.json())
app.dependency_overrides.clear()
back = client.get("/feature")
print("cleared", back.json())
$ python di_overrides.py
prod {'env': 'prod', 'feature_x': False}
override {'env': 'test', 'feature_x': True}
cleared {'env': 'prod', 'feature_x': False}
Production settings report feature_x: False. Assigning app.dependency_overrides[get_settings] = fake_settings flips the flag for the next request without touching environment variables. Clearing the dict restores the original callable. This is the same mechanism you use to inject a fake user or an in-memory database during pytest.
Common errors and fixes
- Calling the dependency yourself: write
Depends(get_db), notDepends(get_db()). FastAPI must receive the callable. - Forgetting Annotated:
user: dict = Depends(get_current_user)still works, but the 2026 docs and type checkers preferAnnotated[dict, Depends(get_current_user)]. - Cleanup never runs: use a single
yieldand put teardown infinally. Raising beforeyieldskips the handler; raising after still runs other dependencies' exit code in order. - Overrides leak between tests: always
app.dependency_overrides.clear()(or restore the previous dict) in a fixture teardown. - 401/403 from a dependency: that is expected. Raise
HTTPExceptioninside the dependency so every route that depends on it shares the same auth errors.
Where to use this next
- Load typed environment config once and inject it with Depends using pydantic-settings: Typed Env Config in Python.
- Drive the same dependency graph from pytest with FastAPI Testing with Pytest and TestClient.
- Replace the fake DB with a real ORM session pattern from SQLAlchemy 2.0 ORM in Python.
Conclusion
FastAPI dependency injection turns shared setup into composable callables: declare them with Annotated and Depends, nest them for auth and permissions, use yield for reliable cleanup and override them in tests. Once those four patterns are in place, new routes stay thin and the hard parts (sessions, users, settings) stay in one tested place.