FastAPI’s dependency injection system is its most powerful architectural feature — it makes shared logic (database sessions, authentication, settings, rate limiting) reusable across route handlers with zero code duplication. When you declare a parameter as Depends(some_function), FastAPI calls some_function, resolves its own parameters (which may themselves be dependencies), and passes the result to your handler. The system handles lifecycle (setup and teardown), caching (calling the same dependency only once per request), and testing (replacing real dependencies with fakes via app.dependency_overrides).
How Depends() Works
from fastapi import FastAPI, Depends
app = FastAPI()
# ── A simple dependency — a regular function ──────────────────────────────────
def get_greeting(name: str = "World") -> str:
return f"Hello, {name}!"
@app.get("/greet")
def greet(message: str = Depends(get_greeting)):
return {"message": message}
# GET /greet → {"message": "Hello, World!"}
# GET /greet?name=Alice → {"message": "Hello, Alice!"}
# FastAPI resolves get_greeting's parameters from the request,
# calls get_greeting(name="Alice"), and passes the result to greet()
# ── Dependency resolving its own dependencies ─────────────────────────────────
def get_base_url(request: Request) -> str:
return str(request.base_url)
def get_config(base_url: str = Depends(get_base_url)) -> dict:
return {"base_url": base_url, "version": "1.0.0"}
@app.get("/info")
def get_info(config: dict = Depends(get_config)):
return config # base_url automatically extracted from request
get_db, FastAPI calls get_db only once and passes the same session to both. This “request-scoped singleton” behaviour is correct for database sessions (one session per request) but may be wrong for other dependencies. To disable caching for a specific dependency use Depends(get_something, use_cache=False) — FastAPI will call it fresh each time it appears.__call__, or class constructors. Class-based dependencies are useful for grouping related parameters and keeping state between dependency methods. The class is instantiated per-request: class Pagination: def __init__(self, page: int = 1, size: int = 10): ... — FastAPI instantiates Pagination(page=2, size=20) from the request query params and injects the instance.yield (generator dependencies) have setup code before the yield and teardown code after. The teardown code runs after the route handler returns its response — not after the response is sent to the client. This is important for database sessions: db.close() in the finally block of a get_db dependency runs after your handler returns but before the HTTP response is fully sent. Never assume the teardown has run when the client receives the response.Sub-Dependencies
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
# ── Dependency chain: A → B → C ───────────────────────────────────────────────
def get_token(authorization: str | None = Header(default=None)) -> str:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "Missing or invalid Authorization header")
return authorization.removeprefix("Bearer ")
def get_user_id_from_token(token: str = Depends(get_token)) -> int:
# In reality: decode JWT, validate signature, extract sub claim
if token == "test-token-user-5":
return 5
raise HTTPException(401, "Invalid token")
def get_current_user(user_id: int = Depends(get_user_id_from_token)) -> dict:
user = fake_db.get(user_id)
if not user:
raise HTTPException(401, "User not found")
return user
@app.get("/me")
def my_profile(user: dict = Depends(get_current_user)):
return user
# FastAPI calls: get_token() → get_user_id_from_token() → get_current_user()
# Each depends on the result of the previous
yield Dependencies — Lifecycle Management
from typing import Generator
from sqlalchemy.orm import Session
# ── yield-based dependency: setup → handler → teardown ───────────────────────
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db # handler runs here, receiving db
db.commit() # commits if no exception
except Exception:
db.rollback() # rolls back if exception raised
raise
finally:
db.close() # always closes (returns to pool)
# ── Async yield dependency ────────────────────────────────────────────────────
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# ── Multiple yield dependencies in one request ────────────────────────────────
# Both run their teardown in LIFO order (last-in, first-out):
# get_db teardown runs before get_redis teardown
@app.get("/data")
def get_data(
db: Session = Depends(get_db),
redis: Redis = Depends(get_redis),
user: User = Depends(get_current_user),
):
...
Common Mistakes
Mistake 1 — Calling the dependency function directly (not using Depends)
❌ Wrong — bypasses FastAPI’s injection, session lifecycle, caching:
@app.get("/posts")
def list_posts():
db = get_db() # calls generator, gets generator object, NOT the session!
return db.query(Post).all() # TypeError
✅ Correct — always use Depends():
@app.get("/posts")
def list_posts(db: Session = Depends(get_db)): # ✓
return db.scalars(select(Post)).all()
Mistake 2 — Database work after yield in get_db
❌ Wrong — db.close() runs after handler returns; any DB work after close fails:
def get_db():
db = SessionLocal()
yield db
db.commit() # Works
result = db.query(Post).count() # After commit: may use closed connection
✅ Correct — do all database work in the handler (before the yield returns control).
Mistake 3 — Not catching exceptions in yield dependency teardown
❌ Wrong — exception in handler bypasses commit/rollback:
def get_db():
db = SessionLocal()
yield db
db.commit() # skipped if handler raised an exception!
✅ Correct — use try/except/finally to ensure teardown always runs.
Quick Reference
| Pattern | Code |
|---|---|
| Simple dependency | param = Depends(function) |
| Disable caching | Depends(fn, use_cache=False) |
| Yield dependency | def dep(): setup(); yield value; teardown() |
| Sub-dependency | Dependency function declares its own Depends() |
| Router-level dep | APIRouter(dependencies=[Depends(require_auth)]) |
| App-level dep | app.include_router(router, dependencies=[Depends(rate_limit)]) |