Depends(fn) tells FastAPI to call fn, pass any of its own parameters through
the same injection system, and give the result to the handler as the argument.
from fastapi import Depends, FastAPI
app = FastAPI()
def get_query(q: str | None = None):
return q
@app.get("/items")
async def list_items(query: str | None = Depends(get_query)):
return {"q": query}
Benefits:
- Reusable logic (auth, DB sessions, pagination) across many handlers.
- Testable — swap dependencies via
app.dependency_overrides. - Recursive — a dependency can itself declare
Depends().
Rule of thumb: extract any logic that repeats across handlers into a dependency function; if it's in more than two handlers, it probably belongs in a dep.
Yes — dependency functions participate in the same parameter extraction system
as route handlers. They can declare path params, query params, headers, cookies,
and other Depends().
from fastapi import Depends, Query
def pagination(
page: int = Query(ge=1, default=1),
size: int = Query(ge=1, le=100, default=20),
) -> dict:
return {"skip": (page - 1) * size, "limit": size}
@app.get("/users")
async def list_users(paging: dict = Depends(pagination)):
return await db.get_users(**paging)
@app.get("/orders")
async def list_orders(paging: dict = Depends(pagination)):
return await db.get_orders(**paging)
Rule of thumb: put shared query parameters (pagination, sorting, filtering) into a dependency class or function — define once, reuse everywhere.
Yes — FastAPI resolves the full dependency graph recursively. Sub-dependencies are resolved before the dependencies that need them.
def get_db():
...
yield session
def get_current_user(db = Depends(get_db)):
user = db.query(User).first()
return user
def require_admin(user = Depends(get_current_user)):
if not user.is_admin:
raise HTTPException(403, "Admin only")
return user
@app.delete("/users/{id}")
async def delete_user(id: int, admin = Depends(require_admin)):
...
FastAPI calls get_db → get_current_user → require_admin in order, caching each
result within the request.
Rule of thumb: chain dependencies to encode privilege escalation cleanly —
require_admin composes get_current_user which composes get_db.
Once per request (by default). FastAPI caches dependency results within a single request — the same object is passed to both dependants.
def get_db():
print("get_db called")
yield session
def dep_a(db = Depends(get_db)): ...
def dep_b(db = Depends(get_db)): ...
@app.get("/")
async def handler(a = Depends(dep_a), b = Depends(dep_b)):
# "get_db called" prints once, not twice
...
To force a fresh call (bypass cache):
Depends(get_db, use_cache=False)
Rule of thumb: rely on caching to share a single DB session across all dependencies in a request — it prevents phantom double-writes or double-reads.
Define a class with __init__ taking FastAPI-injectable parameters, then
use the class itself (not an instance) as the argument to Depends:
from fastapi import Depends, Query
class PaginationDep:
def __init__(
self,
page: int = Query(ge=1, default=1),
size: int = Query(ge=1, le=100, default=20),
):
self.skip = (page - 1) * size
self.limit = size
@app.get("/items")
async def list_items(paging: PaginationDep = Depends()):
return await db.get(skip=paging.skip, limit=paging.limit)
Depends() (with no argument) is shorthand for Depends(PaginationDep) when
the type annotation is already the class.
Rule of thumb: use class-based dependencies for multi-parameter groups (pagination, filtering) — they're more readable than a function returning a tuple of values.
Yes — any callable can be a dependency, including instances with __call__:
class VerifyToken:
def __init__(self, required_scope: str):
self.required_scope = required_scope
def __call__(self, token: str = Depends(oauth2_scheme)):
payload = decode_jwt(token)
if self.required_scope not in payload.get("scopes", []):
raise HTTPException(403, "Insufficient scope")
return payload
verify_read = VerifyToken("read")
verify_write = VerifyToken("write")
@app.get("/items", dependencies=[Depends(verify_read)])
async def list_items(): ...
@app.post("/items", dependencies=[Depends(verify_write)])
async def create_item(): ...
Rule of thumb: use callable instances when a dependency needs to be parameterised at definition time (scope, role, feature flag) — it avoids writing multiple near-identical dependency functions.
When the type annotation already names the dependency class, you can write
Depends() (no argument) and FastAPI infers the class from the annotation:
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0):
self.q = q
self.skip = skip
@app.get("/items")
async def list_items(commons: CommonQueryParams = Depends()):
# ^^^ shorthand
return commons.__dict__
This only works in Python 3.10+ (or with from __future__ import annotations)
and requires the annotation to be a class directly, not a string or Optional.
Rule of thumb: use the shorthand for class-based deps to reduce repetition;
use the explicit Depends(SomeFunction) for function-based deps (no shorthand there).
Pass it to the dependencies= list in the route decorator. The dependency runs
but its return value is discarded.
from fastapi import Depends
async def rate_limit():
if too_many_requests():
raise HTTPException(429, "Too many requests")
@app.get("/items", dependencies=[Depends(rate_limit)])
async def list_items():
return []
The same dependency can be applied at the router level:
router = APIRouter(dependencies=[Depends(rate_limit)])
Rule of thumb: use dependencies=[...] for side-effect deps (auth checks,
rate limiting, audit logging) where the return value is not needed in the handler.
Pass dependencies to the FastAPI() constructor:
from fastapi import FastAPI, Depends
async def verify_api_key(x_api_key: str = Header()):
if x_api_key != settings.api_key:
raise HTTPException(401)
app = FastAPI(dependencies=[Depends(verify_api_key)])
Every single route in the app now runs verify_api_key first. To exclude
specific routes (e.g., the /health check), structure them under a different
router that doesn't inherit the global dependency.
Rule of thumb: global dependencies are powerful but inflexible — prefer router-level dependencies so you can have public and protected route groups.
Any Python value — FastAPI passes whatever the function returns to the handler parameter:
# returns a simple value
def current_user_id(token: str = Depends(oauth2_scheme)) -> int:
return decode_jwt(token)["sub"]
# returns a Pydantic model
def current_user(token: str = Depends(oauth2_scheme)) -> User:
return User.model_validate(decode_jwt(token))
# returns None (side-effect dep)
def check_rate_limit():
if over_limit():
raise HTTPException(429)
@app.get("/me")
async def me(user: User = Depends(current_user)):
return user
Rule of thumb: annotate the return type of dependency functions — it documents what the handler receives and helps type checkers catch mismatches.
The exception propagates up through the dependency chain and FastAPI returns the error response to the client — the handler never runs.
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except JWTError:
raise HTTPException(
status_code=401,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
return payload
@app.get("/me")
async def me(user: dict = Depends(get_current_user)):
return user # only reached if get_current_user doesn't raise
Rule of thumb: raise HTTPException in dependencies rather than returning
sentinel values — the handler should never need to check "did the dep succeed?".
Use app.dependency_overrides dict. The key is the original function; the value
is the replacement callable:
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies import get_current_user
def mock_user():
return {"sub": 1, "role": "admin"}
app.dependency_overrides[get_current_user] = mock_user
client = TestClient(app)
def test_admin_endpoint():
resp = client.get("/admin/stats")
assert resp.status_code == 200
# Clean up after the test
app.dependency_overrides.clear()
Rule of thumb: always clear dependency_overrides after each test — a stale
override in one test silently affects subsequent tests.
More Dependency Injection interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.