← Back to FastAPI
Lesson 4.2 · Dependencies

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.

Intermediate35 min

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.

workflowThe dependency chainstep 1 / 3

1 - No token

get_current_user raises 401. Nothing after it runs.

/me
401 Missing or wrong X-Token
session opened
yes - get_db ran first

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.

Small dictionary
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.
Example 1 - deps.py
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"]}
try_deps.py
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.
Output
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".
extras.py
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}
Output
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.
Output - parameters in /docs
/me [('x-token', 'header')] /notes [('page', 'query'), ('size', 'query'), ('x-token', 'header')] /admin/stats [('x-token', 'header')]
test_notes.py
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"}
Output - python -m pytest -q -s test_notes.py
. 1 passed in 0.10s

Depends in practice at a glance

Chain

A dependency asks for others.

def get_current_user(db: DB, x_token: ...): ...
Stop the request

From any dependency.

raise HTTPException(status_code=401, detail="...")
Class

Empty Depends() = the class itself.

paging: Annotated[Pagination, Depends()]
With a setting

An object with __call__.

Depends(RequireRole("reader"))
Run again

Not 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.

Use /docs

“Run fastapi dev deps.py, open /docs and call /notes with X-Token t-ravi. Where do you type the header?”

An async dependency

“Make get_current_user async def. Does anything change in the output?”

Two roles

“Change RequireRole to accept several roles: RequireRole("reader", "admin").”

Count sessions

“Call /admin/stats as anu. How many sessions are opened, and why?”

What usually goes wrong

Checking the token in every endpoint

Put it in one dependency and ask for CurrentUser.

Expecting a new session for each use

Within one request the session is shared. Use use_cache=False only when you need two.

Depends(Pagination()) for a class

That makes one object when the file loads. Use Depends() or Depends(Pagination).

✗ Depends(Pagination())
✓ Annotated[Pagination, Depends()]
Heavy work before cheap checks

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.

1.

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 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

How many sessions did "notes as anu" open?
One (#3), shared by get_current_user and my_notes.
No token and page=0: which code came back?
401 - the dependency raised before the 422 was reported.
What does Depends() with no argument mean?
Use the type from the annotation (here the Pagination class) as the dependency.
What does use_cache=False change?
The dependency runs again instead of sharing the first result in that request.

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. 1.

    Why does /admin/stats show an x-token header in /docs?

  2. 2.

    Why did the override test open no session?

  3. 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...