FastAPI Dependency Overrides — Deep Dive
How dependency resolution actually works
FastAPI’s dependency system is built on top of Starlette and uses a resolver that walks the dependency graph for each request. When a route declares Depends(get_db), FastAPI:
- Checks
app.dependency_overridesforget_dbas a key - If found, calls the override function instead
- If not found, calls
get_dbdirectly - Caches the result for that request (dependencies are resolved once per request by default)
The lookup uses Python’s identity comparison (is), not equality. This is why you must reference the exact function object — not a re-import or a wrapper.
# routes.py
from app.deps import get_db
@app.get("/items")
async def list_items(db: Session = Depends(get_db)):
return db.query(Item).all()
# test_routes.py
from app.deps import get_db # Same import path = same object
def get_test_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = get_test_db
If your test imports get_db from a different path (e.g., from app.routes import get_db after it was re-exported), you may end up with a different object, and the override silently does nothing.
Async override patterns
FastAPI handles both sync and async dependencies. Overrides must match the async nature of the original:
# Original async dependency
async def get_redis():
r = await aioredis.from_url("redis://localhost")
yield r
await r.close()
# Override must also be async
async def get_mock_redis():
yield FakeRedis()
app.dependency_overrides[get_redis] = get_mock_redis
Mixing sync originals with async overrides (or vice versa) causes TypeError at runtime. FastAPI detects whether a dependency is a coroutine and dispatches accordingly — the override must match.
Generator vs return overrides
Dependencies that use yield (generator dependencies) have setup and teardown phases. Overrides can also use yield:
# Production: connection pool with teardown
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Test: in-memory DB with rollback
def get_test_db():
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
try:
yield session
finally:
transaction.rollback()
connection.close()
The teardown after yield runs after the response is sent, just like the original. This makes overrides suitable for transactional test patterns where you roll back every test’s changes.
Factory and parameterized overrides
Sometimes you need the override to behave differently per test. Use a factory:
def make_user_override(role: str = "admin", user_id: int = 1):
async def override():
return User(id=user_id, role=role, email=f"test-{user_id}@example.com")
return override
# In test
app.dependency_overrides[get_current_user] = make_user_override(role="viewer")
This avoids polluting a shared test user across tests that need different roles or permissions.
Nested dependency overrides — targeting the right layer
Consider a dependency chain: get_current_user → get_token → get_settings. You have three override points:
| Override target | Effect |
|---|---|
get_settings | get_token and get_current_user still run, but receive mock settings |
get_token | get_current_user still runs, but receives a fabricated token; get_settings is never called |
get_current_user | Nothing in the chain runs; you provide the final user directly |
Choosing the right level depends on what you’re testing. Override get_current_user to test route logic in isolation. Override get_token to test your user-resolution logic with a known token. Override get_settings to test the full chain under different configuration.
Test architecture with overrides
A well-structured test setup for a FastAPI project:
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.deps import get_db
SQLALCHEMY_TEST_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_TEST_URL, connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=engine)
@pytest.fixture(autouse=True)
def db_override():
Base.metadata.create_all(bind=engine)
def override():
db = TestSession()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override
yield
app.dependency_overrides.clear()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client():
from fastapi.testclient import TestClient
return TestClient(app)
Key architectural decisions:
autouse=Truefor database overrides ensures every test gets isolation without explicit fixture requestsclear()in teardown prevents cross-test contamination- Schema create/drop per test gives complete isolation at the cost of speed; for large suites, use transaction rollback instead
Overriding class-based dependencies
FastAPI supports class-based dependencies via __call__. Overriding them requires creating an instance of the override class:
class RateLimiter:
def __init__(self, calls_per_minute: int = 60):
self.limit = calls_per_minute
async def __call__(self, request: Request):
# Check rate limit against Redis
...
class NoOpRateLimiter:
async def __call__(self, request: Request):
return None # Always allow
rate_limiter = RateLimiter()
app.dependency_overrides[rate_limiter] = NoOpRateLimiter()
Note that the key is the instance, not the class. This is because Depends(rate_limiter) references the instance.
Performance considerations
Dependency overrides have negligible runtime cost — it’s a dictionary lookup per dependency per request. However, test suite architecture choices matter:
- In-memory SQLite is fast but doesn’t support PostgreSQL-specific features (JSON operators, array types,
ON CONFLICT) - Transaction rollback (start transaction → test → rollback) is faster than create/drop schema but requires careful handling of nested transactions
- Testcontainers (spinning up a real PostgreSQL in Docker) gives full fidelity but adds seconds per suite
The right choice depends on your fidelity needs. Most teams use transaction rollback for unit tests and Testcontainers for integration tests.
Debugging override failures
When an override doesn’t seem to take effect:
- Verify identity: Add
print(id(get_db))in both your route module and test module. If the IDs differ, you have an import aliasing problem. - Check timing: Overrides must be set before the
TestClientmakes the request. If you set them after constructing the client, the client may have already resolved the app. - Inspect the dict: Print
app.dependency_overridesright before the request to confirm your function is in there. - Sub-application mounts: If your route lives in a sub-application (mounted via
app.mount), you need to override on that sub-app’s instance, not the parent.
Real-world example: multi-tenant test isolation
A SaaS application with tenant-scoped data access:
# Production dependency
async def get_tenant(request: Request, db: Session = Depends(get_db)):
tenant_id = request.headers.get("X-Tenant-ID")
tenant = db.query(Tenant).filter_by(id=tenant_id).first()
if not tenant:
raise HTTPException(status_code=404)
return tenant
# Test override factory
def make_tenant_override(tenant_name: str = "Acme Corp"):
async def override():
return Tenant(id=1, name=tenant_name, plan="enterprise")
return override
# Test: verify billing endpoint respects tenant plan
def test_billing_shows_enterprise_features(client):
app.dependency_overrides[get_tenant] = make_tenant_override("Acme Corp")
response = client.get("/billing/features")
assert "priority-support" in response.json()["features"]
This pattern scales to testing any per-tenant behavior without setting up real tenant records in the database.
The one thing to remember: Dependency overrides work by function identity in a simple dictionary — understand the resolution chain, match async signatures, clean up after tests, and you’ll have a test suite that’s fast, isolated, and resistant to refactoring.
See Also
- Python Aiohttp Client Understand Aiohttp Client through a practical analogy so your Python decisions become faster and clearer.
- Python Api Client Design Why building your own API client in Python is like creating a TV remote that only has the buttons you actually need.
- Python Api Documentation Swagger Swagger turns your Python API into an interactive playground where anyone can click buttons to try it out — no coding required.
- Python Api Mocking Responses Why testing with fake API responses is like rehearsing a play with stand-ins before the real actors show up.
- Python Api Pagination Clients Why APIs send data in pages, and how Python handles it — like reading a book one chapter at a time instead of swallowing the whole thing.