A yield dependency is a generator function that uses yield to split into
setup (before yield) and teardown (after yield). FastAPI runs the setup,
injects the yielded value, runs the handler, then runs the teardown — even if
the handler raises an exception.
from sqlalchemy.orm import Session
from app.db import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db # handler receives this
finally:
db.close() # always runs after the handler
@app.get("/users")
async def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
This is the standard pattern for DB sessions, file handles, and any resource that must be cleaned up after the request.
Rule of thumb: use yield deps instead of separate open/close functions —
the finally block guarantees cleanup even when the handler raises.
Use async def with yield — the same pattern but awaitable:
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import async_session_factory
async def get_async_db():
async with async_session_factory() as session:
yield session
await session.commit() # auto-commit on success
# async context manager handles close
Or more explicitly:
async def get_async_db():
session = AsyncSession(engine)
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
Rule of thumb: for async databases always use an async yield dep — mixing sync
SQLAlchemy sessions with async def handlers blocks the event loop.
Code after yield still runs (inside the finally block), but code after
yield and outside finally does not if an exception propagated.
def get_db():
db = SessionLocal()
try:
yield db
db.commit() # ← skipped if handler raises
except Exception:
db.rollback() # ← runs if handler raises, then re-raises
raise
finally:
db.close() # ← always runs
FastAPI catches the exception from the handler, runs the dep's except/finally
blocks, then re-raises the exception so the error response is still sent to the client.
Rule of thumb: always put resource cleanup in finally, never rely on code
after yield running unconditionally — it only runs on success.
FastAPI runs teardowns in reverse order of setup — LIFO (last-in, first-out). If dep A was set up before dep B, B tears down before A.
def dep_a():
print("A setup")
yield "a"
print("A teardown") # runs second
def dep_b():
print("B setup")
yield "b"
print("B teardown") # runs first
@app.get("/")
async def handler(a = Depends(dep_a), b = Depends(dep_b)):
return {}
# output: A setup, B setup, B teardown, A teardown
This mirrors context manager nesting: inner closes before outer.
Rule of thumb: if dep B depends on A's resource (e.g., B uses the DB session from A), declare B's dependency on A explicitly so the order is guaranteed.
Teardown code (after yield) runs after the response is sent to the client
— the same lifecycle as BackgroundTasks. You cannot modify the response headers
at this point, but you can perform cleanup I/O.
def audit_dep(request: Request):
start = time.time()
yield
elapsed = time.time() - start
# log_access(request.url, elapsed) ← safe here, response already sent
You cannot raise HTTPException in teardown (after yield) and expect it to
change the response — the response is already sent.
Rule of thumb: teardown code is for cleanup only (close sessions, release locks, write audit logs); never try to alter the HTTP response there.
| Concern | Middleware | Dependency |
|---|---|---|
| Applies to ALL routes | ✅ easy | Needs global dependencies= |
| Access to response body | ✅ | ❌ |
| Access to handler's return value | ❌ | ✅ (yield after) |
Testable via dependency_overrides |
❌ | ✅ |
| Needs route parameters | ❌ (no routing yet) | ✅ |
| Short-circuit before routing | ✅ | ❌ |
# Middleware — good for request timing, response modification
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start)
return response
# Dependency — good for auth (needs decoded token in handler)
async def get_current_user(token = Depends(oauth2_scheme)): ...
Rule of thumb: use middleware for HTTP-level concerns (timing, CORS, compression); use dependencies for application-level concerns (auth, DB sessions, feature flags).
Return a dependency function from a factory function:
from fastapi import Depends, HTTPException
def require_role(role: str):
def _check(user: User = Depends(get_current_user)):
if user.role != role:
raise HTTPException(403, f"Role '{role}' required")
return user
return _check
@app.get("/admin", dependencies=[Depends(require_role("admin"))])
async def admin_panel(): ...
@app.get("/reports", dependencies=[Depends(require_role("analyst"))])
async def reports(): ...
Or use a class with __call__ (see "callable instance dep" pattern).
Rule of thumb: parametrised deps via factory functions are cleaner than a dozen
require_admin, require_analyst, etc. — the role is a parameter, not a copy.
Use contextlib.asynccontextmanager or just write the yield dep directly. If
the resource is already an async context manager, delegate to it:
import httpx
from fastapi import Depends
async def get_http_client():
async with httpx.AsyncClient() as client:
yield client # handler gets the open client
@app.get("/proxy")
async def proxy(client: httpx.AsyncClient = Depends(get_http_client)):
resp = await client.get("https://api.example.com/data")
return resp.json()
FastAPI closes the AsyncClient after the handler runs.
Rule of thumb: wrap third-party async context managers (httpx, aiofiles) in yield deps — you get proper lifecycle management without handler boilerplate.
Per-request only. FastAPI creates a new dependency cache for each incoming
request; there is no cross-request singleton via Depends().
For cross-request singletons (connection pools, ML models, HTTP clients):
- Store them on
app.statein thelifespanfunction. - Or use a module-level variable /
@lru_cache.
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.http_client = httpx.AsyncClient()
yield
await app.state.http_client.aclose()
@app.get("/proxy")
async def proxy(request: Request):
client = request.app.state.http_client # cross-request singleton
return await client.get("https://api.example.com/")
Rule of thumb: use lifespan + app.state for long-lived resources (DB pool,
ML model); use Depends() for per-request resources (sessions, auth context).
Request →
Middleware (outermost first) →
FastAPI routing →
Global app dependencies →
Router-level dependencies →
Route-level dependencies →
Handler body
← Route-level dep teardown
← Router-level dep teardown
← Global dep teardown
← FastAPI exception handling
← Middleware (innermost first on way out)
Response
Middleware wraps everything, including dependency setup/teardown. Global deps set up before router deps, which set up before route deps.
Rule of thumb: auth middleware that checks a header before FastAPI even routes the request is faster than a dep (no routing overhead) but less testable; choose based on whether you need route-aware logic.
Security utilities are callable classes that implement SecurityBase. When
you use them with Depends(), FastAPI:
- Extracts the token (from header/cookie/query).
- Adds the scheme to the OpenAPI security definitions automatically.
- Makes Swagger UI show the "Authorize" button.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
@app.get("/me")
async def me(token: str = Depends(oauth2_scheme)):
# token is the Bearer value from "Authorization: Bearer <token>"
...
Without security utilities, you'd read the header manually and lose the OpenAPI integration.
Rule of thumb: always use FastAPI's security utilities (OAuth2PasswordBearer,
HTTPBearer, APIKeyHeader) instead of plain Header() for auth — the OpenAPI
integration is a meaningful developer-experience benefit.
Declare Request as a parameter of the dependency function — FastAPI injects it
automatically without Depends():
from fastapi import Request, Depends
def get_client_ip(request: Request) -> str:
return request.client.host
@app.get("/items")
async def list_items(ip: str = Depends(get_client_ip)):
return {"client_ip": ip}
The Request object gives access to headers, cookies, query string, client
address and the raw body stream.
Rule of thumb: inject Request into dependencies that need HTTP metadata
(rate limiting by IP, request ID logging) — keep handlers clean of low-level
HTTP details.
More Dependency Injection interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.