FastAPI Dependency Injection — How and Why to Use It
Master FastAPI's dependency injection system with Depends().
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- FastAPI Depends() declares functions that run before your route logic — framework handles resolution and injection
- Dependencies enable Inversion of Control: you declare what an endpoint needs, FastAPI handles the how
- yield dependencies act as context managers — setup before yield, teardown in finally, even on exceptions
- Sub-dependencies nest recursively: Auth -> Role Check -> DB Lookup creates a validation graph
- By default FastAPI caches dependency results per request — set use_cache=False for side-effect dependencies
- Biggest mistake: copy-pasting auth checks into every route instead of centralizing with Depends()
FastAPI's dependency injection system is a built-in mechanism for declaring and resolving the dependencies your endpoint functions need to run. Unlike traditional DI containers (like Spring or Guice) that focus on wiring up entire object graphs, FastAPI's approach is lighter and more Pythonic: you define dependencies as callable functions (or classes) that accept their own dependencies, and FastAPI automatically resolves the chain at request time.
This solves the real-world problem of endpoint code becoming cluttered with repetitive boilerplate—like parsing query parameters, validating authentication tokens, or opening database sessions—by moving that logic into reusable, testable components. The key insight is that FastAPI's DI isn't just about decoupling; it's about leveraging Python's type hints to let the framework handle the plumbing, so your endpoint functions stay focused on business logic.
In practice, this means you can create a dependency for something as simple as shared query parameters (e.g., pagination skip and limit) and inject it into multiple endpoints with a single Depends() declaration. For authentication, you can build a dependency that checks a JWT token, raises an HTTPException if invalid, and returns the user object—allowing downstream endpoints to branch logic based on the user's role.
The yield pattern extends this further for resource management: a dependency can open a database session, yield it to the endpoint, and then automatically close or roll back the session after the response is sent, even if an exception occurs. This eliminates the need for manual try/finally blocks in every route.
FastAPI's DI is not a replacement for full-fledged DI frameworks in large enterprise applications—if you need complex lifecycle management or AOP-style interceptors, you'd still reach for something like inject or dependency-injector. But for the vast majority of FastAPI services (APIs, microservices, serverless functions), it's the right tool because it's zero-config, deeply integrated with the framework's validation and OpenAPI generation, and forces you to write explicit, type-safe dependencies.
When you shouldn't use it: if your dependencies have heavy initialization costs that should be shared across requests (use lru_cache or singletons instead), or if you're building a non-web Python app where FastAPI's async request context doesn't apply.
Think of dependency injection like a restaurant kitchen. The chef (your endpoint) does not go shopping for ingredients (auth, DB sessions, config). The kitchen manager (FastAPI) delivers exactly what the recipe calls for before cooking starts. If the ingredient is unavailable (auth fails), the order never reaches the chef. And if two dishes need the same ingredient, the manager sources it once and shares it across both — no duplication, no waste.
The yield pattern extends this analogy: after the meal is served, the kitchen manager also handles cleanup — washing the dishes, returning equipment to storage — even if something went wrong during cooking. You do not have to remember to clean up. The system guarantees it.
FastAPI's dependency injection system turns repetitive endpoint boilerplate into reusable, testable components that resolve automatically at request time. For production APIs, this means authentication checks, database sessions, and shared query parameters no longer clutter your route handlers or drift out of sync across endpoints. The Depends() mechanism leverages Python type hints to wire up dependency graphs with zero configuration, while the yield pattern gives you request-scoped resource management without manual try/finally blocks. If you're building FastAPI services at scale, mastering this system is non-negotiable—it directly eliminates entire categories of bugs around resource leaks, inconsistent validation, and forgotten auth checks.
Why FastAPI Dependency Injection Is More Than a DI Container
FastAPI dependency injection is a framework-level mechanism that resolves function parameters by type annotation before your endpoint runs. Unlike traditional DI containers that manage object graphs and lifetimes globally, FastAPI's DI is per-request: each dependency is instantiated when a request arrives and torn down when it completes. The core mechanic is simple — declare a parameter with a type hint, and FastAPI calls the corresponding callable to produce the value. This turns cross-cutting concerns like database sessions, authentication, and configuration into pluggable components that your endpoint code never constructs directly.
What makes this powerful in practice is that dependencies can themselves depend on other dependencies, forming a directed acyclic graph that FastAPI resolves automatically. Each dependency can have its own lifecycle (singleton, per-request, or per-connection) via the Depends() callable. Critically, dependencies are just regular Python functions or classes — no decorators, no base classes, no magic. This means you can unit-test them in isolation and reuse them across endpoints without framework coupling.
You reach for FastAPI DI when you need to share state or behavior across multiple endpoints without duplicating code. The canonical use cases are database session management (open a session on request start, commit or rollback on finish), authentication (extract and validate tokens, then inject the current user), and configuration (load settings once and inject them where needed). In production systems, this pattern eliminates entire categories of bugs: no forgotten session closes, no inconsistent auth checks, no hardcoded config values scattered across route handlers.
too many clients errors.Basic Dependency — Shared Query Parameters
A dependency is just a standard Python callable — a function, a class, or anything with a __call__ method. By wrapping it in Depends(), you tell FastAPI to treat that callable's parameters as if they were declared directly on the endpoint. FastAPI introspects the signature, resolves the parameters from the incoming request (query string, headers, body), and injects the return value into your route handler.
This is the cleanest way to standardize pagination, filtering, sorting, or any shared input-processing logic across your entire API. The key insight is that the dependency function signature becomes part of the API contract — query parameters declared inside a dependency appear in the auto-generated OpenAPI schema exactly as if they were declared on the endpoint itself. API consumers see a consistent interface. Your codebase has one source of truth.
In production, this pattern eliminates an entire class of bugs: parameter drift across endpoints. Without DI, you copy the pagination logic into every route. Six months later, one endpoint caps limit at 100 and another allows limit=10000. One endpoint validates that skip is non-negative and another silently accepts skip=-5. The dependency pattern makes these inconsistencies structurally impossible — there is one function, one validation path, one default value. Changing it changes every endpoint that uses it simultaneously.
from fastapi import FastAPI, Depends, Query, HTTPException, status from typing import Annotated app = FastAPI() def common_params( skip: int = Query(default=0, ge=0, description="Number of records to skip"), limit: int = Query(default=100, ge=1, le=1000, description="Maximum records to return"), ) -> dict: """ Standardized pagination parameters for all list endpoints. Enforces a hard cap at 1000 to prevent unbounded queries. ge=0 on skip prevents negative offsets that some ORMs handle unpredictably. This function is the single source of truth for pagination logic. Changing the cap or default here applies to every endpoint that uses it — no find-and-replace required. """ return {"skip": skip, "limit": limit} # Type alias for readability — used across multiple route files PaginationParams = Annotated[dict, Depends(common_params)] @app.get("/users") def list_users(params: PaginationParams): """ List users with standardized pagination. Query params 'skip' and 'limit' appear in OpenAPI docs even though they are declared inside common_params, not here. """ # Business logic: fetch users using params["skip"] and params["limit"] return {"context": "users", "pagination": params} @app.get("/orders") def list_orders(params: PaginationParams): """ List orders with the same pagination contract. If the cap changes from 1000 to 500, it changes here and applies to both /users and /orders automatically. """ return {"context": "orders", "pagination": params} @app.get("/products") def list_products(params: PaginationParams, category: str | None = None): """ Dependencies compose cleanly with route-level parameters. category is unique to this endpoint; skip and limit come from the dependency. Both appear in the OpenAPI schema. """ return {"context": "products", "category": category, "pagination": params}
- The dependency function signature IS the contract — its parameters become API inputs visible in OpenAPI docs
- FastAPI validates dependency parameters with the same rules as route parameters — Query constraints, type coercion, required vs optional
- The return value is injected into the endpoint as a typed argument — use Annotated for explicit type documentation
- Multiple endpoints share one dependency — single source of truth for validation logic, defaults, and constraints
- Changing the dependency changes every endpoint that uses it simultaneously — no copy-paste drift possible
Authentication Dependency & Logic Branching
Dependencies are the gatekeepers of your routes. By raising an HTTPException inside a dependency, you stop the request before it reaches your route handler. The route never executes, the business logic never runs, and the response is returned immediately with the error status code. This is not just convenient — it is a security property.
The authentication dependency pattern separates the concern of 'who is this request from' from 'what does this request do.' The endpoint never needs to know how authentication works. It does not parse tokens, query user tables, or check API key formats. It receives an authenticated identity object and operates on it. The auth mechanism is entirely contained in the dependency.
This separation has a direct and concrete security benefit: changing your auth mechanism requires changing one function. JWT to OAuth2, API keys to mutual TLS, single-tenant to multi-tenant — the endpoint code is untouched. Without DI, you would need to audit every route handler individually, and that audit will miss something. There is no version of 'update every route manually' that is reliably complete.
The second security benefit is placement. Applied at the router level, auth dependencies create a structural guarantee: every route in the group requires authentication. There is no way to add a new endpoint and forget the auth check — the router applies it automatically. Applied at the route level, auth is a per-developer discipline that fails the moment someone is moving fast and forgets.
from fastapi import FastAPI, Depends, HTTPException, Header, status, APIRouter from typing import Annotated from dataclasses import dataclass @dataclass class AuthenticatedUser: """ The identity object returned by a successful auth dependency. Returning a structured object instead of a raw dict makes downstream role checks type-safe and self-documenting. Mypy catches accesses to non-existent fields at development time rather than at runtime in production. """ user_id: str role: str scopes: list[str] def validate_api_key( x_forge_token: Annotated[str | None, Header()] = None, ) -> AuthenticatedUser: """ Validate the X-Forge-Token header against the allowed key set. In production, replace the hardcoded check with a lookup against HashiCorp Vault, AWS Secrets Manager, or a secrets database. Never hardcode secrets in source — this example uses a literal only to show the dependency structure. Raises HTTPException 401 if the token is missing or invalid. Returns an AuthenticatedUser if valid — the route receives the identity, not a boolean. """ if not x_forge_token or x_forge_token != "forge-prod-secret": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing Forge API Key", headers={"WWW-Authenticate": "ApiKey"}, ) # In production: look up the key, fetch the associated user and scopes return AuthenticatedUser( user_id="svc-order-service", role="service", scopes=["orders:read", "orders:write"], ) def require_admin( auth: Annotated[AuthenticatedUser, Depends(validate_api_key)], ) -> AuthenticatedUser: """ Sub-dependency for admin-only endpoints. Composes on top of validate_api_key — the request must first pass the base auth check, then pass the role check. FastAPI caches the validate_api_key result, so the token is only validated once even though two dependencies reference it. """ if auth.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Admin role required. Caller role: {auth.role}", ) return auth # Protected router — every route inherits auth at the router level # No individual route can skip auth by forgetting the Depends() annotation protected_router = APIRouter( prefix="/api/v1", dependencies=[Depends(validate_api_key)], ) # Admin router — sub-dependency composes on the base auth check admin_router = APIRouter( prefix="/admin", dependencies=[Depends(require_admin)], ) @protected_router.get("/orders") def list_orders(auth: Annotated[AuthenticatedUser, Depends(validate_api_key)]): """ The route receives the identity object directly. It knows WHO is calling, not just that auth passed. This enables per-user data filtering without a second DB lookup. """ return { "caller": auth.user_id, "scopes": auth.scopes, "orders": [], # Fetch filtered by auth.user_id in production } @admin_router.get("/metrics") def get_internal_metrics(auth: Annotated[AuthenticatedUser, Depends(require_admin)]): """ Only admin-role callers reach this handler. require_admin handles both auth and role — the route is clean. """ return {"metrics": "[REDACTED]", "caller": auth.user_id}
Depends() annotations when the endpoint looks structurally complete without it.The 'Yield' Pattern: Database Session Management
Managing database connections is where FastAPI's dependency injection either saves you or burns you. You must open the session before the route runs, and you must close it — return it to the pool — after the route completes, regardless of whether the route succeeded, raised an HTTPException, or crashed with an unhandled error. 'Regardless of outcome' is the hard part.
FastAPI's yield dependencies solve this by acting as request-scoped context managers. Code before the yield statement runs during setup — opening the session, acquiring any necessary locks, initializing state. Code after the yield in a finally block runs during teardown — closing the session, releasing locks, cleaning up. FastAPI holds a reference to the generator across the entire request lifecycle and resumes it after the response is sent or the exception is handled.
The critical distinction — the one that causes the most production incidents — is between yield dependencies and plain function dependencies with try/finally blocks. In a plain function dependency, the finally block does not execute when the route raises an HTTPException. FastAPI intercepts the exception at the route execution boundary and converts it to a JSON error response. This interception happens before control returns to the plain function's finally block. The database session is never closed. The connection leaks.
Only yield dependencies get guaranteed teardown because FastAPI explicitly resumes the generator after exception handling. The generator is paused at the yield, the exception is handled, the response is sent, and then FastAPI calls next() on the generator to trigger the finally block. This is not automatic — it is a deliberate design in FastAPI's dependency execution model. If you are acquiring any resource in a dependency — database session, HTTP client, file handle, distributed lock — you must use yield. There is no production-safe alternative.
from fastapi import FastAPI, Depends, HTTPException, status from sqlalchemy import create_engine, event, text from sqlalchemy.orm import sessionmaker, Session from sqlalchemy.pool import QueuePool from typing import Annotated, Generator import logging logger = logging.getLogger(__name__) SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/forge_db" engine = create_engine( SQLALCHEMY_DATABASE_URL, poolclass=QueuePool, pool_size=20, # Base connections kept open max_overflow=10, # Additional connections allowed under burst load pool_recycle=300, # Recycle connections after 5 minutes to prevent stale state pool_pre_ping=True, # Test connection health at checkout — catches dropped connections pool_timeout=30, # Raise after 30s waiting for a connection — fail fast, not hang ) SessionLocal = sessionmaker( autocommit=False, autoflush=False, bind=engine, ) def get_db() -> Generator[Session, None, None]: """ Yield a database session for the duration of a single request. MUST use yield — not return. With yield: FastAPI holds the generator open across the request lifecycle. After the route completes (or raises), FastAPI resumes the generator, triggering the finally block that closes the session. Without yield (plain try/finally): FastAPI's exception handling intercepts HTTPException before the finally block executes. The session is never closed. The connection leaks. The pool exhausts. The service returns 503. This is the most common FastAPI production bug — and the most preventable one. """ db = SessionLocal() logger.debug(f"DB session opened: id={id(db)}") try: # Setup phase: session is open and ready yield db # If the route commits explicitly, nothing to do here # If the route did not commit, nothing is committed — no surprise writes except Exception: # Roll back any uncommitted transaction before closing # Prevents partial writes from surviving an exception db.rollback() raise finally: # Teardown phase: ALWAYS runs — success, HTTPException, or crash logger.debug(f"DB session closed: id={id(db)}") db.close() # Type alias — use this in route signatures for readability DBSession = Annotated[Session, Depends(get_db)] app = FastAPI() @app.get("/db-status") def check_db_connection(db: DBSession): """ Health check that validates the DB session is functional. Uses db.execute() to confirm the connection is active, not just that the session object was created. """ try: db.execute(text("SELECT 1")) return {"status": "connected", "session_id": id(db)} except Exception as e: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"Database unreachable: {str(e)}", ) @app.get("/orders/{order_id}") def get_order(order_id: int, db: DBSession): """ Route that uses the injected session for a real query. Whether this raises 404, 422, or succeeds — the session is closed. FastAPI guarantees teardown via the yield generator resume. """ # order = db.query(Order).filter(Order.id == order_id).first() # if not order: # raise HTTPException(status_code=404, detail="Order not found") # return order return {"order_id": order_id, "status": "fetched", "session_id": id(db)} # Example of what NOT to do — shown explicitly so the bug is recognizable in review: # def get_db_wrong(): # db = SessionLocal() # try: # return db # Returns immediately — FastAPI has no generator to resume # except Exception: # db.close() # raise # finally: # db.close() # This finally block DOES NOT RUN when the route raises HTTPException # # FastAPI intercepts the exception before returning here # # Connection leaks. Pool exhausts. 503s at moderate load.
rollback() for transactional resources before close().SessionLocal() creation. Never share a session across requests — sessions are not thread-safe and hold transaction state.Sub-Dependencies: When a Dependency Calls Another Dependency
You will not write flat dependencies. Real endpoints require two, three, maybe four levels of injected logic. A payment endpoint needs an authenticated user, then a valid cart, then a shipping rate calculator that depends on the cart's weight. FastAPI resolves the whole chain automatically. You declare a dependency that takes another dependency. FastAPI instantiates each one, caches the result per request (unless you mark it as non-cached), and passes the return values up the chain. No manual wiring. No singletons to misconfigure. If the shipping service raises an exception, every dependent route handler rejects cleanly without partial state corruption. The key insight: FastAPI does not care which dependency is 'top-level.' It builds a Directed Acyclic Graph from your functions and resolves dependencies in topological order. This is why you never need a separate DI container library. FastAPI's graph is alive at runtime and it respects HTTP lifecycle boundaries.
// io.thecodeforge from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() # Level 1: token extraction async def extract_token(authorization: str | None = None): if not authorization: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) return authorization.split(" ")[-1] # Level 2: user lookup depends on token async def get_current_user(token: str = Depends(extract_token)): # imagine DB lookup if token != "valid-secret": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return {"username": "ops-lead", "role": "admin"} # Level 3: admin check depends on user async def require_admin(user: dict = Depends(get_current_user)): if user["role"] != "admin": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return user @app.get("/admin/secrets") async def read_secrets(admin: dict = Depends(require_admin)): return {"secret": "the coffee machine needs descaling again"}
Overriding Dependencies for Testing Without Mocking Frameworks
You do not need pytest-mock or unittest.patch for most FastAPI dependency tests. FastAPI exposes app.dependency_overrides. It is a plain dict. You replace a dependency function with a test double. The replacement can return a stub, raise an exception, or simulate a database timeout. This works because FastAPI's DI system is entirely functional — it never looks at the actual import path, only at the callable signature. The override happens before the application object receives any requests. You can set it in a fixture, run your TestClient calls, then restore the original in a teardown. This is safer than mocking because you replace the entire injection point, not the internals of a module. If a junior mistakenly patches the wrong function, your tests pass until production breaks. Dependency overrides catch that at the type level. You get a TypeError at test time because the fake returns a string but the route expects a dict. Use this aggressively in every integration test that touches authentication, external APIs, or database sessions.
// io.thecodeforge import pytest from fastapi.testclient import TestClient from app.main import app from app.dependencies import get_db_session, get_current_user # Stub for DB session def stub_session(): return "fake-db-session" # Stub for auth def stub_admin_user(): return {"username": "test-admin", "role": "admin"} @pytest.fixture def override_deps(): app.dependency_overrides[get_db_session] = stub_session app.dependency_overrides[get_current_user] = stub_admin_user yield app.dependency_overrides.clear() @pytest.mark.usefixtures("override_deps") def test_admin_endpoint_returns_secrets(): client = TestClient(app) response = client.get("/admin/secrets") assert response.status_code == 200 assert "coffee" in response.json()["secret"]
Database Connection Pool Exhaustion from Missing Yield in Dependency
db.close() in the dependency's finally block could execute.
FastAPI intercepts HTTPException at the route execution level and converts it to a JSON response. This interception happens before control returns to the dependency's finally block in a plain function. Only yield dependencies get a guaranteed callback after the route completes or raises — because FastAPI holds a reference to the generator and explicitly resumes it after the response is handled.
With a pool of 20 connections and roughly one validation error per second, the pool exhausted in approximately 20 seconds. Each failed request left one connection open, held by a session object that was never closed. The connection eventually timed out and was returned to the pool — but by then, the queue of waiting requests had already triggered a cascade of 503s.- Always use yield for resource-managing dependencies — plain try/finally does not survive FastAPI's exception handling interception
- FastAPI only guarantees teardown for yield dependencies — the generator resume is the mechanism, not the try/finally block
- Increasing pool_size to fix exhaustion without diagnosing the leak treats the symptom — the pool will always exhaust eventually if connections are not being released
- Monitor connection pool utilization with a dedicated metric — alert at 80% so you catch leaks before they cause user-visible failures
- Set pool_pre_ping=True to detect stale connections at checkout rather than failing mid-request
db.close() because FastAPI's exception handling intercepts before the dependency's finally block runs. Convert to yield dependency immediately. Then add a connection count metric to verify the fix — if active connections stabilize below pool_size after the change, the diagnosis was correct.Depends() declarations at different levels.app.dependency_overrides.clear() to prevent state from leaking into the next test.SELECT count(*), state FROM pg_stat_activity WHERE datname = 'your_db' GROUP BY state;grep -c 'QueuePool limit' /var/log/app.logprint(app.routes) # Verify route dependency declarations are present# Add explicit logging inside the dependency to confirm execution and return value:
def get_db():
logger.info('get_db called — creating session')
db = SessionLocal()
try:
logger.info(f'get_db yielding session id={id(db)}')
yield db
finally:
logger.info(f'get_db closing session id={id(db)}')
db.close()app.dependency_overrides[get_db] = lambda: mock_session # Must be the exact same function object# Clear after each test to prevent state leakage:
def teardown():
app.dependency_overrides.clear()# Anti-pattern: D executes twice because B and C both declare Depends(D) with use_cache=False
# A depends on B (which depends on D), A depends on C (which depends on D)
# D runs once for B, once for C — sequential, not cached# Correct pattern: D executes once, result cached and shared
# A depends on B, A depends on C, both B and C depend on D
# FastAPI resolves D first, caches result, injects into both B and C| Dependency Type | Teardown Guarantee | Use Case | Trade-off |
|---|---|---|---|
Plain function Depends() | No — skipped on route exceptions due to FastAPI exception interception | Pure computation: parse token, validate params, extract query args, check header format | No cleanup mechanism — do not use for any resource acquisition, no matter how minor |
Yield Depends() | Yes — finally block runs even on unhandled exceptions, after response is sent | Resource management: DB sessions, HTTP client connections, file handles, distributed locks | Slightly more complex lifecycle — must understand setup/teardown phases and where exceptions fall |
Class-based Depends() | No (unless __call__ uses yield internally) | Stateful logic: service objects, complex configuration, multi-method validators, dependency factories | More verbose and requires understanding of Python callable protocol — worth it for complex reusable services |
| Sub-dependency (nested Depends) | Inherited from the inner dependency type — yield inside sub-dependency is guaranteed | Composed validation chains: Auth -> Role Check -> Permission Lookup -> DB Session | Deep nesting beyond 3 levels becomes hard to trace during debugging — flatten where possible |
| Router-level dependency | Inherited from dependency type — applies to every route in the router | Structural enforcement: all routes get auth, rate limiting, request ID injection, logging | Cannot exempt individual routes without creating a separate router — plan router boundaries during design |
| Global dependency (app level) | Inherited from dependency type — runs on every single request | Cross-cutting concerns: CORS preflight handling, global request ID injection, request timing | Runs on every request including health checks and static file serving — add path exclusions for non-API routes |
| File | Command / Code | Purpose |
|---|---|---|
| sub_dependencies.py | from fastapi import FastAPI, Depends, HTTPException, status | Sub-Dependencies |
| test_overrides.py | from fastapi.testclient import TestClient | Overriding Dependencies for Testing Without Mocking Framewor |
Key takeaways
Depends() is the primary tool for Inversion of Control in FastAPICommon mistakes to avoid
5 patternsUsing a plain function instead of yield for database session dependencies
db.rollback() in an except clause before db.close() to prevent uncommitted transactions from surviving exceptions. Add pool utilization monitoring with an alert at 80% of pool_size — if the fix is working, the metric stabilizes at the concurrent request count instead of growing monotonically.Applying auth dependencies at the route level instead of the router level
Not using use_cache=False for dependencies with side effects that must run independently each time they are referenced
Deep nesting of sub-dependencies creating an untraceable execution graph that is impossible to reason about during incident response
Dependency override not working in integration tests — real services called, real database hit, mock is ignored despite being configured
app.dependency_overrides.clear() to prevent state leakage between tests in the same session.Interview Questions on This Topic
Describe the 'Dependency Graph' resolution in FastAPI. How does it handle a scenario where Endpoint A depends on B and C, while both B and C depend on D?
How does FastAPI ensure that a 'yield' dependency cleans up resources if an unhandled exception occurs inside the route function?
generator.throw()), which causes the exception to propagate through the yield point and into the finally block. The finally block executes, the resource is cleaned up, and FastAPI then re-raises the exception to produce the error response.
This is fundamentally different from a plain function dependency with try/finally. In a plain function, FastAPI calls the function to get the return value and never holds a reference to any cleanup code. When the route raises an HTTPException, FastAPI's exception handling converts it to a JSON response before returning to the caller — which means the dependency's finally block is never triggered because the dependency function already completed when it returned the value.
The design implication is direct: yield is the only mechanism FastAPI provides for guaranteed resource teardown in dependencies. Any dependency that acquires a resource — database session, file handle, HTTP client, distributed lock — must use yield. There is no production-safe alternative.Scenario: You need to implement a 'Soft-Delete' filter globally. How would you use a Router-level dependency to ensure every query in a specific module excludes deleted records?
What are 'Security Dependencies' in FastAPI, and how do they integrate with the auto-generated Swagger (OpenAPI) documentation?
How would you override a dependency during an integration test to avoid hitting a real database?
python
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db
def override_get_db():
db = TestingSessionLocal() # In-memory SQLite or test schema
try:
yield db
finally:
db.close()
# Set override BEFORE creating TestClient
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
# After test:
app.dependency_overrides.clear()
``
Three details that matter in production test suites:
First, the key must be the exact function object — the same import used in the route declaration. If your app imports get_db from app.database and your test imports it differently, they may be different objects and the override will not match.
Second, clear overrides after each test — use a pytest fixture with yield or an autouse teardown. Overrides that leak between tests cause intermittent failures that are nearly impossible to debug.
Third, this pattern works for any dependency — not just database sessions. Override validate_api_key to return a mock AuthenticatedUser, override an external API client to return canned responses, override a feature flag dependency to force-enable a feature in tests. The override system is the primary mechanism for isolating FastAPI route tests from external dependencies.Frequently Asked Questions
A regular function call requires you to provide all arguments manually at the call site. With Depends(), FastAPI takes over: it inspects the dependency's parameter signature, resolves each parameter from the incoming request (query string, headers, path, body), recursively resolves any nested Depends() declarations within the function, caches the result for the request duration, and injects the return value into your route handler.
The key capability that Depends() enables and regular function calls cannot replicate is recursive dependency resolution with per-request caching. If your auth dependency needs a database session, and the database session dependency needs the engine configuration, FastAPI resolves the entire graph automatically in the correct order. A regular call chain requires you to manually thread arguments through every level — which defeats the purpose of the abstraction.
Use use_cache=False when the dependency has side effects that must execute independently each time it is referenced in a single request — not when it should simply be recomputed.
The concrete cases: generating unique idempotency keys (each reference should produce a different value), incrementing a per-reference counter, or acquiring separate independent resource instances rather than sharing one.
Do not use use_cache=False for performance reasons or to force freshness — the cache is per-request, not across requests, so every request always gets a fresh execution. use_cache=False only affects whether multiple references within the same request share a result or each get their own execution.
Document use_cache=False explicitly at the declaration site — it is non-obvious to future readers and easy to misinterpret as a bug.
Yes, and it is particularly useful for complex stateful services or dependency factories where you want method reuse, initialization logic, or configurable behavior.
The pattern: define a class with __init__ to accept parameters (which FastAPI resolves from the request) and __call__ to implement the dependency logic. FastAPI calls the class as a callable, resolving __init__ parameters the same way it resolves function parameters.
For example, a permission checker class that takes the required permission string in __init__ and validates it against the current user in __call__ creates a reusable, configurable dependency: Depends(RequirePermission('orders:write')). Each instance is configured differently but implements the same interface.
Class-based dependencies are more verbose than function dependencies — use them when the added structure pays for itself through reuse, testability, or configuration flexibility. Do not use them as a default pattern.
FastAPI does not provide a built-in dependency execution trace. You need to instrument manually.
For development debugging: add logger.info() at the entry and exit of each dependency function. The log output will show the execution order clearly, including which dependencies are cached (they appear once) and which execute multiple times (use_cache=False or a bug in graph structure).
For test-time tracing: use app.dependency_overrides to inject a wrapper function that logs the call and delegates to the original. This lets you trace execution order without modifying production code.
For production observability: add OpenTelemetry spans inside yield dependencies. Wrap the setup phase in one span and the teardown in another — this gives you timing data for both phases in your distributed trace, separated from the route handler's business logic span. This is particularly useful for diagnosing where latency is accumulating in deep dependency chains.
Every FastAPI concept with runnable in-browser examples — params, Pydantic, dependency injection, JWT auth, async, SQLAlchemy, testing, WebSockets, and Docker deployment. The interactive reference for production engineers.
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
That's Python Libraries. Mark it forged?
5 min read · try the examples if you haven't