Depends in Practice
Real apps chain dependencies: a token header gives the current user, the user gives the admin check, and a database session is shared by all of them. Build that chain, use a class as a dependency, give a dependency a setting, and see that a dependency runs only once per request.
What you will be able to do
- Build a chain of dependencies: session -> user -> admin
- Raise 401 and 403 from dependencies
- Use a class as a dependency with Depends()
- Give a dependency a setting with a callable class
- Understand that a dependency runs once per request, and use_cache=False
- Replace the current user in a test
The idea, in plain English
In Lesson 4.1 one endpoint used one dependency. Real apps have a chain: to know the current user you need the token header and a database session; to check an admin you need the current user. A dependency can ask for other dependencies in exactly the same way an endpoint does.
FastAPI works out the whole chain for every request. It calls each dependency once, in the right order, and shares the result with everyone who asked for it - so the user check and the endpoint get the same database session. If any step raises HTTPException, the request stops there.
We build a small notes API with a fake session (a real database comes in Module 5) and a token header (real login comes in Module 6). FastAPI 0.143.0, pytest 9.
Worked example: A notes API: a fake database session, the current user from an X-Token header, an admin-only page, paging with a class, and a role check with a setting.
1 - No token
get_current_user raises 401. Nothing after it runs.
Each box is a dependency. FastAPI calls each one once per request. Real results from our notes API.
Words you will see in this lesson
A few more words about dependencies.
Sub-dependencyA dependency that another dependency asks for.SessionAn open connection to the database for one piece of work.TokenA secret string a client sends to prove who it is.CallableAnything you can call with (): a function, a class, or an object with __call__.CacheKeeping a result so it is not made again.An everyday example: a hospital visit
To see a specialist, several checks happen in order:
- Reception checks your ID card - the token.
- They find your file - the current user, using the hospital records (the session).
- The specialist’s desk checks you have a referral - the admin check, which needs your file.
- Everyone uses the same file for this visit; nobody makes a new one.
- If reception refuses your ID, you never reach the specialist - the request stops.
Example 1 - a chain of dependencies
Read deps.py from the top:
- FakeSession stands in for a database session. Each new one gets a number and prints "open session #n".
- get_db() makes a session. DB = Annotated[FakeSession, Depends(get_db)].
- get_current_user(db, x_token) asks for TWO things: the session (a dependency) and the X-Token header. Unknown token: 401. CurrentUser is its short type.
- get_admin(user) asks for the current user and raises 403 for non-admins.
- Pagination is a class. Annotated[Pagination, Depends()] - with empty brackets - means "call the class itself". Its __init__ arguments become query parameters.
- my_notes asks for the user, the session AND paging - three dependencies.
import itertools
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, Query
app = FastAPI()
TOKENS = {"t-ravi": "ravi", "t-anu": "anu"} # token -> username
USERS = {"ravi": {"name": "ravi", "role": "reader"},
"anu": {"name": "anu", "role": "admin"}}
NOTES = {"ravi": ["buy milk"], "anu": ["plan release", "hire"]}
session_ids = itertools.count(1)
class FakeSession: # stands in for a database session
def __init__(self):
self.id = next(session_ids)
print(f" open session #{self.id}")
def get_user(self, name: str) -> dict | None:
return USERS.get(name)
def get_db() -> FakeSession:
return FakeSession()
DB = Annotated[FakeSession, Depends(get_db)]
def get_current_user(db: DB, x_token: Annotated[str | None, Header()] = None) -> dict:
if x_token not in TOKENS: # a dependency that uses two others
raise HTTPException(status_code=401, detail="Missing or wrong X-Token")
print(f" get_current_user used session #{db.id}")
return db.get_user(TOKENS[x_token])
CurrentUser = Annotated[dict, Depends(get_current_user)]
def get_admin(user: CurrentUser) -> dict: # a dependency on a dependency
if user["role"] != "admin":
raise HTTPException(status_code=403, detail="Admins only")
return user
class Pagination: # a class as a dependency
def __init__(self, page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=20)] = 5):
self.page = page
self.size = size
self.skip = (page - 1) * size
@app.get("/me")
def me(user: CurrentUser):
return user
@app.get("/notes")
def my_notes(user: CurrentUser, db: DB, paging: Annotated[Pagination, Depends()]):
print(f" my_notes used session #{db.id}")
notes = NOTES[user["name"]]
return {"page": paging.page, "notes": notes[paging.skip: paging.skip + paging.size]}
@app.get("/admin/stats")
def stats(admin: Annotated[dict, Depends(get_admin)]):
return {"users": len(USERS), "asked_by": admin["name"]}from fastapi.testclient import TestClient
from deps import app
c = TestClient(app)
def call(label, url, token=None):
print(label)
headers = {"X-Token": token} if token else {}
r = c.get(url, headers=headers)
print(f" -> {r.status_code} {r.text}")
call("me, no token", "/me")
call("me as ravi", "/me", "t-ravi")
call("notes as anu", "/notes?size=1", "t-anu")
call("notes, page 0", "/notes?page=0", "t-anu")
call("admin as ravi", "/admin/stats", "t-ravi")
call("admin as anu", "/admin/stats", "t-anu")Reading the output
- me, no token - a session opened, then get_current_user stopped the request with 401.
- notes as anu - ONE session (#3), used by get_current_user AND my_notes. FastAPI called get_db once and shared the result.
- notes, page 0 - the user check ran and a session opened, and only then the answer was 422 for page. FastAPI runs every dependency it can, collects the input errors, and answers 422 at the end.
- admin as ravi - 403 from get_admin. admin as anu - 200.
me, no token
open session #1
-> 401 {"detail":"Missing or wrong X-Token"}
me as ravi
open session #2
get_current_user used session #2
-> 200 {"name":"ravi","role":"reader"}
notes as anu
open session #3
get_current_user used session #3
my_notes used session #3
-> 200 {"page":1,"notes":["plan release"]}
notes, page 0
open session #4
get_current_user used session #4
-> 422 {"detail":[{"type":"greater_than_equal","loc":["query","page"],"msg":"Input should be greater than or equal to 1","input":"0","ctx":{"ge":1}}]}
admin as ravi
open session #5
get_current_user used session #5
-> 403 {"detail":"Admins only"}
admin as anu
open session #6
get_current_user used session #6
-> 200 {"users":2,"asked_by":"anu"}Which error wins?
We sent no token AND page=0. The answer was 401, not 422: get_current_user raised HTTPException, which stops the request at once. Input errors are only reported after all dependencies have run.
- A dependency that raises stops everything at once.
- Wrong query values, headers or body are collected and reported together as 422 at the end.
- So expensive work in a dependency (like opening a session) can run for a request that fails with 422 - session #4 above.
A dependency with a setting
get_admin checks one fixed role. To check different roles, make a class whose objects can be called:
- RequireRole("reader") makes an object that remembers the role.
- Depends(...) gets that object. For each request FastAPI calls it - which runs __call__.
- __call__ asks for CurrentUser like any dependency, so the chain still works.
- ravi (reader) got the report; anu (admin) got 403 "Needs role reader".
from typing import Annotated
from fastapi import Depends, HTTPException
from deps import DB, CurrentUser, FakeSession, app, get_db
class RequireRole: # a dependency with a setting
def __init__(self, role: str):
self.role = role
def __call__(self, user: CurrentUser) -> dict: # FastAPI calls the object
if user["role"] != self.role:
raise HTTPException(status_code=403, detail=f"Needs role {self.role}")
return user
@app.get("/reports")
def reports(user: Annotated[dict, Depends(RequireRole("reader"))]):
return {"report": "monthly", "for": user["name"]}
@app.get("/two-sessions")
def two_sessions(db: DB, fresh: Annotated[FakeSession, Depends(get_db, use_cache=False)]):
return {"db": db.id, "fresh": fresh.id}no token AND page 0
open session #1
-> 401 {"detail":"Missing or wrong X-Token"}
reports as ravi
open session #2
get_current_user used session #2
-> 200 {"report":"monthly","for":"ravi"}
reports as anu
open session #3
get_current_user used session #3
-> 403 {"detail":"Needs role reader"}
two sessions
open session #4
open session #5
-> 200 {"db":4,"fresh":5}Once per request - and use_cache=False
By default, a dependency used in many places in ONE request runs once, and everyone gets the same result. That is what you want for a session or the current user.
- In /two-sessions, db used the normal DB and fresh used Depends(get_db, use_cache=False).
- Two sessions were opened (#4 and #5): use_cache=False calls the function again.
- Use it only when you really need a new value each time.
- The cache is per request - the next request gets a new session.
The chain in /docs, and in tests
FastAPI shows every parameter of the whole chain. /admin/stats has no header argument, but get_admin -> get_current_user needs X-Token, so /docs shows it.
- In a test, override get_current_user with a function that returns a fixed user.
- The test needs no token - and no session was opened: when a dependency is replaced, its own dependencies are not called.
/me [('x-token', 'header')]
/notes [('page', 'query'), ('size', 'query'), ('x-token', 'header')]
/admin/stats [('x-token', 'header')]from fastapi.testclient import TestClient
from deps import app, get_current_user
def fake_anu():
return {"name": "anu", "role": "admin"}
def test_admin_stats():
app.dependency_overrides[get_current_user] = fake_anu # no token, no session needed
r = TestClient(app).get("/admin/stats")
app.dependency_overrides.clear()
assert r.status_code == 200
assert r.json() == {"users": 2, "asked_by": "anu"}.
1 passed in 0.10sDepends in practice at a glance
ChainA dependency asks for others.
def get_current_user(db: DB, x_token: ...): ...
Stop the requestFrom any dependency.
raise HTTPException(status_code=401, detail="...")
ClassEmpty Depends() = the class itself.
paging: Annotated[Pagination, Depends()]
With a settingAn object with __call__.
Depends(RequireRole("reader"))Run againNot cached in this request.
Depends(get_db, use_cache=False)
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run fastapi dev deps.py, open /docs and call /notes with X-Token t-ravi. Where do you type the header?”
“Make get_current_user async def. Does anything change in the output?”
“Change RequireRole to accept several roles: RequireRole("reader", "admin").”
“Call /admin/stats as anu. How many sessions are opened, and why?”
What usually goes wrong
Put it in one dependency and ask for CurrentUser.
Within one request the session is shared. Use use_cache=False only when you need two.
That makes one object when the file loads. Use Depends() or Depends(Pagination).
✗ Depends(Pagination())✓ Annotated[Pagination, Depends()]Dependencies ran (and a session opened) even for a request that ended with 422.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Add POST /notes that adds a note for the current user (text as a query parameter, 1-200 characters), and DELETE /users/{name} that only admins can use. Use the existing dependencies; do not check the token inside the endpoints. Then write a pytest test for DELETE that overrides get_current_user with a reader and expects 403.
Show hintHide hint
def delete_user(name: str, admin: Annotated[dict, Depends(get_admin)]). The override still runs get_admin, because only get_current_user is replaced.
Key points
- A dependency can ask for other dependencies, headers, query values and the body.
- HTTPException in any dependency stops the request at once.
- Input errors are collected and answered as 422 after the dependencies have run.
- A dependency runs once per request and its result is shared; use_cache=False repeats it.
- A class works as a dependency; Depends() with no argument uses the type itself.
- A callable object (with __call__) gives a dependency a setting.
- Overriding a dependency also skips its sub-dependencies.
Quick check before you move on
Interview questions
How does FastAPI resolve sub-dependencies?
It builds a dependency graph from signatures, calls each dependency once per request (cached by callable and scopes), injects results into dependants, collects validation errors, and stops immediately on raised exceptions.
How would you implement role-based access with dependencies?
A get_current_user dependency authenticates the request; a parametrised callable (RequireRole("admin")) or small per-role dependencies check authorisation and raise 403. Attach them per route or per router.
Quiz
- 1.
Why does /admin/stats show an x-token header in /docs?
- 2.
Why did the override test open no session?
- 3.
How does RequireRole("reader") get the current user?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...