Lesson 2 - Reusable and Sub-Dependencies
Welcome to Reusable and Sub-Dependencies
In the last lesson, a dependency was a single function that returned a value. That’s the foundation, but real applications need more. Sometimes you want a dependency to hand back a typed object instead of a loose dictionary. Sometimes one piece of shared logic needs another piece of shared logic underneath it — checking the current user requires first checking the API key. And sometimes a dependency manages a resource that must be opened before the request and closed after it, like a database session.
FastAPI handles all three with the same Depends you already know. This lesson covers the patterns that take you from “I can share a function” to “I can structure the shared layer of a real Task Manager API.” We’ll go one pattern at a time, show the code, run it, and read the actual output.
By the end of this lesson, you will be able to:
- Use a class as a dependency to get a typed object back from
Depends() - Build a sub-dependency — a dependency that itself depends on another dependency
- Use
yieldto set up a resource before the request and tear it down after, even on errors - Recognize where each pattern fits in a real Task Manager API
Let’s start with classes.
Class Dependencies: A Typed Object Instead of a Dict
A dependency doesn’t have to be a function — it can be a class. When you use a class as a dependency, FastAPI reads the parameters of its __init__ method, collects them from the request just like it would for a function, and then constructs an instance of the class. Your endpoint receives that instance.
Why bother? Because you get a real, typed object with attributes and editor autocomplete, instead of a dictionary where you have to remember the keys. Here’s a Paginator class that captures the same skip and limit parameters from last lesson:
from fastapi import FastAPI, Depends
app = FastAPI()
class Paginator:
def __init__(self, skip: int = 0, limit: int = 10):
self.skip = skip
self.limit = limit
@app.get("/items")
def items(p: Paginator = Depends()):
return {"skip": p.skip, "limit": p.limit}Look closely at p: Paginator = Depends(). There is no argument inside Depends(). Normally you’d write Depends(some_function), but when the dependency is a class, FastAPI can use the type annotation — here Paginator — to know what to construct. So Depends() with no argument means “use the type on the left.” (You could also write it the explicit way, Depends(Paginator); both work.)
The __init__ parameters become query parameters, exactly as a function dependency’s parameters would. Let’s confirm by passing skip=2 and leaving limit to its default:
from fastapi.testclient import TestClient
client = TestClient(app)
print(client.get("/items?skip=2").json()){'skip': 2, 'limit': 10}skip came from the query string; limit fell back to its default of 10. Inside the endpoint, p is a genuine Paginator object — you write p.skip, not p["skip"] — which means typos become errors and your editor can autocomplete the attributes. For a small bag of values that’s a modest win; for richer dependencies that hold helper methods alongside their data, a class is the natural home.
Sub-Dependencies: Dependencies That Depend on Dependencies
Here’s where the pattern gets powerful. A dependency is an ordinary function, and ordinary functions can have their own Depends parameters. When dependency B declares that it depends on dependency A, FastAPI resolves the whole chain for you: it runs A first, feeds A’s result into B, and feeds B’s result into your endpoint. This is a sub-dependency.
The classic example is authentication. Checking the current user is really two steps: first verify the API key is valid, then build the user from it. We can split that into two dependencies where the second depends on the first:
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
def get_api_key(x_api_key: str = Header()):
if x_api_key != "secret":
raise HTTPException(status_code=401, detail="bad api key")
return x_api_key
def get_current_user(api_key: str = Depends(get_api_key)): # sub-dependency
return {"user": "ada", "via_key": api_key}
@app.get("/me")
def me(user: dict = Depends(get_current_user)):
return userRead the chain from the bottom up. The /me endpoint depends on get_current_user. But get_current_user doesn’t read the header itself — it depends on get_api_key, which reads the X-API-Key header (FastAPI maps the parameter name x_api_key to the X-API-Key header automatically) and either returns the key or raises 401. Only if get_api_key succeeds does get_current_user run and produce the user.
Note: the “API key” here is just a header string we compare against
"secret"for teaching. It isn’t connected to any real service or credential.
Let’s send a valid key:
from fastapi.testclient import TestClient
client = TestClient(app)
print(client.get("/me", headers={"x-api-key": "secret"}).json()){'user': 'ada', 'via_key': 'secret'}The whole chain resolved: get_api_key validated the header and returned "secret", that value flowed into get_current_user as api_key, and the resulting user reached the endpoint. Now a bad key:
response = client.get("/me", headers={"x-api-key": "nope"})
print(response.status_code)
print(response.json())401
{'detail': 'bad api key'}The chain stopped at the first link. get_api_key raised HTTPException(401), so get_current_user never ran and the endpoint body never executed — the client got a clean 401. This is the real shape of authentication in FastAPI apps: a low-level credential check that several higher-level dependencies build on top of, each reusable on its own.
yield Dependencies: Setup Before, Teardown After
The last pattern handles resources that must be opened before a request and closed after it — a database session, a file handle, a network connection. For these you write a dependency with yield instead of return. Everything before the yield is setup; the value you yield is injected into the endpoint; and everything after the yield runs as teardown once the response is done.
To make the teardown bulletproof, you put it in a finally block. That guarantees it runs whether the endpoint succeeds or raises an error. Here’s a fake database session that records each step in a module-level events list so we can watch the order:
from fastapi import FastAPI, Depends
app = FastAPI()
events = []
def get_db():
events.append("open")
db = {"tasks": ["a", "b"]}
try:
yield db
finally:
events.append("close")
@app.get("/db-count")
def db_count(db: dict = Depends(get_db)):
return {"count": len(db["tasks"])}The flow is: get_db appends "open", creates the fake session, and yields it. The endpoint receives that session as db and counts the tasks. After the response is built, control returns to get_db, the finally block runs, and it appends "close". Let’s trigger one request and then inspect what happened:
from fastapi.testclient import TestClient
client = TestClient(app)
print(client.get("/db-count").json())
print(events){'count': 2}
['open', 'close']The endpoint saw a working session and returned the count. And the events list proves the lifecycle: "open" was recorded before the endpoint ran, "close" after it finished. You never had to remember to close anything in the endpoint itself — the dependency owns the resource’s whole life. In Module 5 you’ll use exactly this pattern with a real database session: open it before the request, commit or roll back and close it after.
Teardown runs even if the endpoint errors
Because the teardown lives in a finally block after the yield, it runs no matter how the request ends — success, an HTTPException, or an unexpected crash inside the endpoint. That’s the whole point: a database session or file gets closed every time, so you never leak resources even on the error paths. Always put cleanup in finally, not just after the yield.
Practice Exercises
Exercise 1: Why no argument in Depends()?
In the items endpoint you saw p: Paginator = Depends() with empty parentheses, but earlier dependencies were written Depends(pagination) with the function inside. Why can the class version leave the parentheses empty, and what’s the equivalent explicit form?
Hint
When the dependency is a class, FastAPI can read the type annotation on the parameter (Paginator) to know what to construct, so Depends() with no argument means “use that annotated type.” The explicit equivalent is Depends(Paginator) — both build a Paginator from the request and inject the instance.
Exercise 2: Trace the chain
A request hits /me with the header x-api-key: nope. Walk through which of get_api_key, get_current_user, and the endpoint body actually run, and what the client receives.
Hint
Only get_api_key runs. It sees "nope" != "secret" and raises HTTPException(status_code=401), so the chain stops there: get_current_user and the /me body never execute. The client receives 401 with {'detail': 'bad api key'}. Sub-dependencies short-circuit as soon as one link fails.
Exercise 3: When does “close” happen?
Using the get_db dependency, suppose the /db-count endpoint raised an error after receiving the session instead of returning cleanly. Would "close" still be appended to events? Why?
Hint
Yes. The teardown is inside a finally block, which always runs when control leaves the try — including when the endpoint raises. So "close" is appended even on the error path, which is exactly why yield plus finally is the safe way to manage a resource like a database session.
Summary
You moved past basic dependencies into three patterns real apps rely on. A class dependency uses a class’s __init__ parameters as the request inputs and injects a typed instance — write Depends() with no argument and let the annotation (like Paginator) say what to build. A sub-dependency is a dependency that itself uses Depends on another dependency; FastAPI resolves the chain top to bottom, as with get_current_user building on get_api_key, and short-circuits with a clean error if a lower link fails. A yield dependency runs setup before the yield and teardown in a finally after it, so resources like a database session are opened before the request and closed after — even if the endpoint errors. The events list ['open', 'close'] showed that lifecycle exactly.
Key Concepts
- Class dependency — a class used with
Depends(); its__init__params become request inputs and you get a typed instance. Depends()with no argument — uses the parameter’s type annotation to know what to construct.- Sub-dependency — a dependency that declares
Depends(...)on another dependency; FastAPI resolves the whole chain. yielddependency — setup beforeyield, teardown infinallyafter; guarantees cleanup even on errors.
Why This Matters
These patterns are how the shared layer of a real FastAPI app is actually built. Class dependencies give you typed, reusable helpers instead of fragile dictionaries. Sub-dependencies let you compose small, single-purpose checks (verify the key, then build the user) into larger ones without duplication. And yield dependencies are the standard way to manage database sessions and other resources safely — you’ll use the exact pattern from this lesson when you connect a real database in Module 5. Together they let your shared logic scale as cleanly as your endpoints do.
Next Steps
Continue to Lesson 3 - Bigger Applications with Routers
Split a growing app into multiple files with APIRouter so each feature lives in its own module.
Back to Module Overview
Return to the Structure, Dependencies, and Middleware module overview
Continue Building Your Skills
You can now reach for the right dependency shape for the job: a class when you want a typed object, a sub-dependency when one piece of shared logic builds on another, and yield when a resource needs setting up and tearing down. Next you’ll take the single-file app you’ve been growing and split it across multiple files with routers, so each feature of the Task Manager lives in its own tidy module.