Dependency Injection
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against FastAPI 0.142.2 (released 2026-09-30), Pydantic 2.13 and Starlette 1.7, checked on
2026-10-02. All code uses Python 3.10+ syntax and Annotated, and every example was executed with TestClient
on Python 3.13; the printed results shown are the real ones. The running scenario is the Bookshelf API
(books, authors and reviews). Dependency injection (DI) is the mechanism FastAPI uses to hand a path operation the
things it needs — pagination settings, a database session, the current user, a settings object — without the
path operation knowing how to build them. It is the single most reused idea in the framework: security, database
sessions, settings and test doubles are all dependencies. This page covers declaring and composing them; the
yield form that adds cleanup code is on Dependencies with yield,
and the security dependencies built on top are on
Security Fundamentals,
OAuth2 Password Flow & JWT and
OAuth2 Scopes & Authorization.
What Dependency Injection Solves
Every non-trivial API repeats the same preparatory work in front of its handlers: parse and validate the same
pagination parameters, open a database session, find out who is calling, check that they may do this. Without a
mechanism, that work is either copy-pasted (and drifts), or hidden in global state (and becomes untestable), or moved
into middleware (which runs for every route and cannot receive typed values). A dependency is an ordinary callable — a function, a class, an object with __call__ — that the path operation declares instead of calling. FastAPI
calls it for you, passes it whatever it declares, and injects the result.
| Problem | What a dependency gives you |
|---|---|
The same query parameters on many endpoints |
One definition; the parameters are validated and documented on every route that uses it. |
Shared resources (database session, HTTP client, settings) |
Created per request (or cached), passed in, replaceable in tests. |
Authentication and authorization |
A |
Business preconditions (the book exists, the caller owns it) |
A dependency that loads or checks and raises |
Testing without a real database or identity provider |
|
The principle is the same as in Spring (@Autowired, see Spring Boot REST APIs) or
.NET constructor injection, with one important difference: FastAPI resolves dependencies per request, from the
function signature, with no container configuration, no registration step and no decorators on the dependency itself.
The signature is the declaration. Where dependencies fit in the life of a request is shown on
Introduction & Architecture.
The first example removes duplication. Both endpoints need skip, limit and q; they are declared once, in an
async function, and injected with Depends:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
BOOKS = [{"id": i, "title": f"Book {i}"} for i in range(1, 8)]
async def common_parameters(skip: int = 0, limit: int = 3, q: str | None = None) -> dict:
return {"q": q, "skip": skip, "limit": limit}
@app.get("/books")
def list_books(commons: Annotated[dict, Depends(common_parameters)]):
items = [b for b in BOOKS if not commons["q"] or commons["q"] in b["title"]]
return items[commons["skip"]: commons["skip"] + commons["limit"]]
@app.get("/authors")
def list_authors(commons: Annotated[dict, Depends(common_parameters)]):
return {"params": commons}
Source: FastAPI: Dependencies
Running it prints:
[{'id': 3, 'title': 'Book 3'}, {'id': 4, 'title': 'Book 4'}]
{'params': {'q': 'tolkien', 'skip': 0, 'limit': 3}}
['query', 'limit']
[('skip', 'query'), ('limit', 'query'), ('q', 'query')]
Three things to notice. The dependency’s own parameters (skip, limit, q) are interpreted by exactly the same
rules as a path operation’s parameters (see Query Parameters): a failed
conversion is reported as a normal 422 with the location ['query', 'limit']. The same declaration is merged into
each operation’s OpenAPI document (the last line, read from /openapi.json). And the dependency was written as async def
although one handler is def: the two styles mix freely, see Sync and Async Dependencies.
The parameter type in Annotated[dict, Depends(common_parameters)] is only for your editor and type checker;
FastAPI does not validate the returned value against it. What Depends receives is the callable, not a call:
Depends(common_parameters), never Depends(common_parameters()).
What Can Be a Dependency
Depends accepts any callable. FastAPI inspects its signature, so "declares what it needs" works the same way for all
of these:
| Callable | Typical use | Notes |
|---|---|---|
|
Resolve a header, load a row, build a client |
Run in a worker thread so a blocking call does not stop the event loop. |
|
Anything that can |
Runs on the event loop; never block in it. See Async & Concurrency. |
Generator / async generator ( |
Setup and teardown |
Covered on Dependencies with yield. |
Class |
Group related parameters into an object with attributes |
The class is called to create an instance: |
Instance with |
Configurable ("parameterized") dependency |
|
|
Pre-fill arguments of an existing function |
Supported since 0.123.5, see functools.partial Dependables. |
Sync and Async Dependencies
A def dependency is run in the external thread pool, an async def one on the event loop, regardless of whether the
path operation itself is def or async def. The following example, with an async def path operation, records which
thread each dependency ran on:
import threading
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
def where() -> str:
return "worker thread" if threading.current_thread().name.startswith("AnyIO") else "event loop thread"
def sync_dep() -> str:
return "sync dependency on " + where()
async def async_dep() -> str:
return "async dependency on " + where()
@app.get("/t")
async def t(a: Annotated[str, Depends(sync_dep)], b: Annotated[str, Depends(async_dep)]):
return [a, b]
['sync dependency on worker thread', 'async dependency on event loop thread']
The practical rule follows: a dependency that calls a blocking library (a synchronous database driver, requests,
file I/O) should be def; one that awaits (an async driver, httpx.AsyncClient) should be async def. A
blocking call inside an async def dependency stalls every request on that worker. Thread-pool capacity and its limiter
are discussed on Async & Concurrency.
Classes as Dependencies
FastAPI only needs a callable, and a class is callable: calling it creates an instance. Its __init__ parameters
are then analysed like any function’s, so a class gives you a typed object with attributes and editor completion
instead of a bare dict:
from typing import Annotated
from fastapi import Depends, FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
class Pagination:
def __init__(
self,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
):
self.skip = skip
self.limit = limit
PaginationDep = Annotated[Pagination, Depends()] # class type is the dependable
@app.get("/books")
def books(p: PaginationDep):
return {"type": type(p).__name__, "skip": p.skip, "limit": p.limit}
@app.get("/authors")
def authors(p: Annotated[Pagination, Depends(Pagination)]): # explicit form
return {"skip": p.skip, "limit": p.limit}
Source: FastAPI: Classes as Dependencies
{'type': 'Pagination', 'skip': 5, 'limit': 10}
Input should be less than or equal to 100
[('skip', None), ('limit', 100)]
The last line is read from the OpenAPI document: the constraint le=100 declared in __init__ became the
maximum of the limit parameter. Validation keywords (Query(ge=0), Path(…), Header(…)) work inside
dependencies exactly as they do in a path operation.
The Depends() Shortcut
When the parameter’s type annotation is the class to instantiate, repeating it inside Depends is redundant, so
Depends() without an argument means "use the annotated type as the callable":
| Written | Equivalent to |
|---|---|
|
|
|
the explicit form; required when the callable is not the annotation, e.g. |
The shortcut only works when the annotation is a callable whose signature FastAPI can read. It is therefore a
class-only trick. Annotated[dict, Depends()] fails when the route is declared (ValueError: no signature found
for builtin type <class 'dict'>, executed on FastAPI 0.142.2): there is no way to build a plain dict from request
data.
Using a class has one more benefit: the type of the injected value is the class, so p.skip is checked by the type
checker, which a dict of strings never gives you. A Pydantic model can be a dependency as well, but it is then
filled from the request body, not from __init__ parameters; for query-string models use Annotated[Model, Query()]
(see Query Parameters) and for headers and cookies the matching Header() and
Cookie() models in Headers & Cookies.
Reusable Annotated Aliases
Annotated[Pagination, Depends()] is long and appears on every endpoint. Because Annotated[…] is an ordinary
Python expression, it can be given a name once and imported everywhere. This is the idiom the official documentation
recommends (it has been supported since 0.95.0, see Python Types & Annotated);
the first example above already used PaginationDep and the Bookshelf scenario below builds a whole vocabulary of such
aliases:
from typing import Annotated
from fastapi import Depends
SessionDep = Annotated[Session, Depends(get_session)]
SettingsDep = Annotated[Settings, Depends(get_settings)]
CurrentUser = Annotated[User, Depends(get_current_active_user)]
Page = Annotated[Pagination, Depends()]
@app.get("/books")
def list_books(session: SessionDep, page: Page, user: CurrentUser): ...
Benefits: a handler’s signature reads as a list of capabilities, the dependency can be changed in one place, and
the type checker sees the real type (session is a Session, not an Annotated wrapper). Keep aliases in a
dependencies.py module beside the routers (see the layout in
Introduction & Architecture).
Sub-Dependencies and the Dependency Tree
A dependency can itself declare dependencies, with no limit on depth. FastAPI builds a tree from the path operation
down, solves it from the leaves up, and caches each result for the request. The Bookshelf API’s current-user chain is a
good model: a settings object feeds a session, the session and a header feed get_current_user, which feeds
get_current_active_user, which the handler declares.
create_review()"] --> CAU["get_current_active_user"] H --> SESS["get_session"] H --> RS["require_scopes('reviews:write')"] CAU --> CU["get_current_user"] CU --> SESS CU --> HDR["Header: Authorization"] RS --> CAU SESS --> SET["get_settings"] SET --> ENV[("environment / .env")]
The figure is the shape of a real authorization setup. get_session and get_settings are shared: the session is
needed both directly by the handler and indirectly by get_current_user, and the settings feed the session. The
caching rule in Per-Request Caching guarantees that each box is evaluated once per request, however many
arrows point at it. require_scopes is a parameterized dependency (see Parameterized Dependencies):
it is a factory that builds a checker which itself depends on the current user.
The first three layers of that tree, with simple fakes, look like this (a fake token table stands in for a real identity provider; the settings and database are placeholders):
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
calls: list[str] = []
class Settings(BaseModel):
app_name: str = "Bookshelf"
class Session:
def __init__(self, settings: Settings):
self.dsn = f"sqlite:///{settings.app_name.lower()}.db"
class User(BaseModel):
name: str
active: bool
FAKE_USERS = {"token-ann": User(name="ann", active=True), "token-bob": User(name="bob", active=False)}
def get_settings() -> Settings:
calls.append("get_settings")
return Settings()
def get_session(settings: Annotated[Settings, Depends(get_settings)]) -> Session:
calls.append("get_session")
return Session(settings)
def get_current_user(
session: Annotated[Session, Depends(get_session)],
x_token: Annotated[str, Header()],
) -> User:
calls.append("get_current_user")
user = FAKE_USERS.get(x_token)
if user is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token",
headers={"WWW-Authenticate": "Bearer"})
return user
def get_current_active_user(user: Annotated[User, Depends(get_current_user)]) -> User:
calls.append("get_current_active_user")
if not user.active:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Inactive user")
return user
@app.get("/me")
def me(user: Annotated[User, Depends(get_current_active_user)],
session: Annotated[Session, Depends(get_session)]):
return {"user": user.name, "dsn": session.dsn}
Source: FastAPI: Sub-dependencies
Three requests, with the list of dependencies that ran printed after each:
200 {'user': 'ann', 'dsn': 'sqlite:///bookshelf.db'}
['get_settings', 'get_session', 'get_current_user', 'get_current_active_user']
400 {'detail': 'Inactive user'} ['get_settings', 'get_session', 'get_current_user', 'get_current_active_user']
422 {'detail': [{'type': 'missing', 'loc': ['header', 'x-token'], 'msg': 'Field required', 'input': None}]} ['get_settings', 'get_session']
The first run shows the order: leaves first (get_settings), then each caller, then the path operation.
get_settings appears once although two dependencies need it. The second request shows an HTTPException raised
from a sub-dependency: the chain stops there and the client gets the 400; the path operation never runs. The third request
carries no X-Token header. get_current_user cannot run, FastAPI reports a 422, and the outcome is worth remembering:
independent leaves (get_settings, get_session) had already run by then. Dependencies are solved before the
request is rejected for a validation error in a sibling, so a dependency should never assume that the request will be
accepted. This matters most for dependencies that open resources; the way yield dependencies are cleaned up in that
situation is on Dependencies with yield.
Two design guidelines come from the tree:
-
One responsibility per node.
get_current_userauthenticates (who is it?),get_current_active_userchecks state (is the account usable?),require_scopesauthorizes (may they do this?). Splitting them lets a public-but-personalised endpoint use only the first and an admin endpoint use all three. -
Raise
HTTPExceptionin the dependency, not in the handler, when the failure is generic (401,403,404for a missing parent). The error format is described in HTTPException: Detail and Headers.
Per-Request Caching and use_cache=False
If the same dependency is reached through several paths in one request, FastAPI calls it once and reuses the result.
The cache key is the dependable callable object itself (plus, for security dependencies, the set of scopes), and the
cache lives only as long as the request, so the next request starts empty. This is what makes get_session safe to
declare in get_current_user and in the handler: both receive the same object, so the user lookup and the handler’s
queries share one transaction.
The following example counts calls. make_id is reached through left, through right and directly:
from itertools import count
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
counter = count(1)
def make_id() -> int:
return next(counter)
def left(x: Annotated[int, Depends(make_id)]) -> int:
return x
def right(x: Annotated[int, Depends(make_id)]) -> int:
return x
@app.get("/cached")
def cached(a: Annotated[int, Depends(left)], b: Annotated[int, Depends(right)],
c: Annotated[int, Depends(make_id)]):
return [a, b, c]
@app.get("/fresh")
def fresh(a: Annotated[int, Depends(make_id, use_cache=False)],
b: Annotated[int, Depends(make_id, use_cache=False)]):
return [a, b]
[1, 1, 1]
[2, 2, 2]
[3, 4]
/cached returns one value per request (1 on the first, 2 on the second) because the three uses collapse into one
call. /fresh passes use_cache=False, so each declaration gets its own call (3 and 4). Use it when the dependency is
not idempotent by design: it returns a fresh value on purpose (a unique id, a timestamp, a random token), or each
user needs an independent resource. The default is the right choice for sessions, settings and the current user.
Caching rules worth knowing:
-
Identity, not equality. Two lambdas with the same body, or two
GenreChecker(…)instances built from the same arguments, are different dependables and are not shared (see Parameterized Dependencies). Create the instance once at module level and reuse that one. -
Per request, not per application. To share a value between requests (a connection pool, a loaded model) create it in the lifespan and read it from
app.state, or cache a settings function withfunctools.lru_cache; see Settings, Lifespan & Background Tasks. -
Yield dependencies are cached in the same way; their exit code runs once. Whether that happens before or after the response is explained, with the
scopeoption, on Dependencies with yield. -
Do not rely on side effects. A dependency that logs, counts or publishes an event is "called once per request" only because of the cache; with
use_cache=Falseor a second callable it would run again.
Dependencies at Every Level: Decorator, Router, Application
Some dependencies are not about a value: they are gates (check a key, rate limit, require authentication) or side effects (set a request id, log). Their return value is not needed, so FastAPI lets you attach them without a parameter. There are four attachment points, from narrowest to widest:
| Where | Syntax | Applies to |
|---|---|---|
Parameter of the handler |
|
That path operation, and you receive the value. |
Path operation decorator |
|
That path operation only; the return value is discarded. |
Router |
|
Every operation of the router. |
|
|
Every operation of that router, for this inclusion only (the same router can be mounted twice with different rules). |
Application |
|
Every path operation of the app, including routers included afterwards. |
The next example attaches a logging dependency at each level, plus a token gate on the router, and records the order:
from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, Header, HTTPException
order: list[str] = []
def log(name: str):
def dep():
order.append(name)
return dep
async def verify_token(x_token: Annotated[str, Header()]) -> str:
order.append("verify_token")
if x_token != "fake-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
return "never used" # return value of decorator dependencies is discarded
def own_dep() -> str:
order.append("parameter dependency")
return "own"
app = FastAPI(dependencies=[Depends(log("global"))])
router = APIRouter(prefix="/books", dependencies=[Depends(verify_token), Depends(log("router"))])
@router.get("/", dependencies=[Depends(log("decorator"))])
def list_books(own: Annotated[str, Depends(own_dep)]):
order.append("path function")
return {"own": own}
app.include_router(router, dependencies=[Depends(log("include_router"))])
@app.get("/public")
def public():
order.append("public handler")
return {"ok": True}
200 {'own': 'own'}
['global', 'include_router', 'verify_token', 'router', 'decorator', 'parameter dependency', 'path function']
400 {'detail': 'X-Token header invalid'} ['global', 'include_router', 'verify_token']
422 ['global', 'include_router', 'router', 'decorator', 'parameter dependency']
{'ok': True} ['global', 'public handler']
[{'name': 'x-token', 'in': 'header', 'required': True, 'schema': {'type': 'string', 'title': 'X-Token'}}]
What this shows (all from the run above):
-
Order. Application, then
include_router, then the router, then the decorator, then the handler’s own parameters, and within one list in the order written. Rely on the order only when you must; independent gates are better made order-independent. -
Return values are discarded (
verify_tokenreturns a string nobody sees). If you need the value, declare it as a parameter as well; the cache makes the second use free. -
A failing gate stops the chain. The
400came fromverify_token, and later dependencies and the handler did not run. -
Missing inputs. The gate’s own parameters (
X-Tokenheader) are validated like any other; the third request without the header produced a422instead of the400, and, as in the earlier example, dependencies that did not need that header had already run (the log showsrouter,decoratorandparameter dependency). -
Global dependencies do not run for the docs.
/docs,/redocand/openapi.jsonare served by Starlette routes that are not path operations, so a global gate does not protect them. Protect or disable them explicitly; see OpenAPI & Interactive Docs. -
OpenAPI. The router-level
X-Tokenheader appears on the operation (last line). Details in How Dependencies Appear in OpenAPI.
For the application-level form, a security gate is the usual use, shown on Security Fundamentals. Remember that a global dependency also applies to routes you add by mistake without thinking about it: keep global dependencies minimal (request id, rate limiting, an optional API key) and put role checks on routers.
Source: FastAPI: Global Dependencies
Parameterized Dependencies
Sometimes the same logic is needed with different configuration: "this endpoint accepts these genres, that one
accepts those", "this route needs scope books:write`". The dependency itself cannot take arguments (FastAPI decides what
to pass), so you build a dependable with the arguments baked in, in one of two ways: a class with `__call__, or a factory
function that returns an inner function.
A callable instance
FastAPI inspects `__call__’s signature; the constructor’s arguments are yours:
from functools import partial
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query
app = FastAPI()
class GenreChecker:
def __init__(self, allowed: set[str]):
self.allowed = allowed
def __call__(self, genre: Annotated[str, Query()]) -> str:
if genre not in self.allowed:
raise HTTPException(422, f"Genre must be one of {sorted(self.allowed)}")
return genre
fiction = GenreChecker({"fantasy", "sf"})
@app.get("/fiction")
def fiction_books(genre: Annotated[str, Depends(fiction)]):
return {"genre": genre}
def page(skip: int, limit: int, max_limit: int) -> dict:
return {"skip": skip, "limit": min(limit, max_limit)}
small_page = partial(page, max_limit=5)
PageDep = Annotated[dict, Depends(small_page)]
@app.get("/page")
def get_page(p: PageDep):
return p
Source: FastAPI: Advanced Dependencies
{'genre': 'sf'}
{'detail': "Genre must be one of ['fantasy', 'sf']"}
{'skip': 0, 'limit': 5}
['skip', 'limit', 'max_limit']
The instance fiction is created once at import time; a request calls it with the request’s genre. The first
two lines are the GenreChecker; the last two belong to the next section.
A factory function: require_scopes
The factory form is the one used for authorization. require_scopes("books:write") is evaluated when the route is
declared, returns the inner checker, and that is the dependency. A fake token table keeps the example runnable:
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
scopes: list[str]
USERS = {"fake-ann": User(name="ann", scopes=["books:read", "books:write"]),
"fake-bob": User(name="bob", scopes=["books:read"])}
def get_current_user(x_token: Annotated[str, Header()]) -> User:
user = USERS.get(x_token)
if not user:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unknown token")
return user
def require_scopes(*required: str):
def checker(user: Annotated[User, Depends(get_current_user)]) -> User:
missing = [s for s in required if s not in user.scopes]
if missing:
raise HTTPException(status.HTTP_403_FORBIDDEN, f"Missing scopes: {missing}")
return user
return checker
@app.delete("/books/{book_id}")
def delete_book(book_id: int, user: Annotated[User, Depends(require_scopes("books:write"))]):
return {"deleted": book_id, "by": user.name}
fake-ann 200 {'deleted': 1, 'by': 'ann'}
fake-bob 403 {'detail': "Missing scopes: ['books:write']"}
nobody 401 {'detail': 'Unknown token'}
The 401 ("who are you?") and 403 ("you may not") separation is deliberate and worth keeping in real code. This
hand-rolled checker uses a plain Header(); for real tokens and the integrated, OpenAPI-visible form, Security()
with SecurityScopes, see OAuth2 Scopes & Authorization.
Pitfalls specific to this pattern:
-
No caching across instances.
require_scopes("a")andrequire_scopes("a")build two different inner functions, so the user lookup beneath them is cached (it is shared) but the checkers themselves run twice. Share instances when it matters. -
Create dependables at import time, not inside the handler. A dependency built per request cannot be overridden in tests because the override key is the object that was created at declaration time.
-
Mutable state in the instance (a counter, a list) is shared by all concurrent requests; keep instances immutable or protect the state.
functools.partial Dependables
Since FastAPI 0.123.5 (2025-12-02), functools.partial objects (and wrapped callables) are accepted. This is the lightest way to
pre-fill arguments of an existing function without writing a class or factory. In the /page endpoint of the
previous example, small_page = partial(page, max_limit=5) fixed max_limit, and FastAPI exposed only the remaining
parameters skip and limit (max_limit is shown in the OpenAPI list above too, because a partial keyword
remains a default and so stays visible and overridable by the client; if you must not expose it, use the factory
form instead). Check the exact behaviour in the run: the response clamped limit=50 to 5.
Source: FastAPI: Release Notes, 0.123.5
Where Parameters Can Come From Inside a Dependency
A dependency’s signature is read exactly like a handler’s, so it can pull values from every part of the request and the handler receives the result, already validated:
from typing import Annotated
from fastapi import Depends, FastAPI, Header
app = FastAPI()
def trace_and_flag(
x_trace: Annotated[str | None, Header()] = None, # header
flag: bool = False, # query
) -> dict:
return {"x_trace": x_trace, "flag": flag}
@app.get("/a")
def a(d: Annotated[dict, Depends(trace_and_flag)]):
return d
Source: FastAPI: Dependencies
{'x_trace': 't1', 'flag': True}
[('flag', 'query'), ('x-trace', 'header')]
The mapping: path template names are path parameters, singular types are query parameters, Header() / Cookie() /
Form() / File() are explicit markers, and a Pydantic model is the request body (see
Path Parameters, Query Parameters and
Request Body & Models). A dependency that declares a body model
makes every handler using it expect that body; combined with Form() fields it switches the whole operation to
form encoding (see Forms & File Uploads). The raw Request or
Response can be requested too, which is how a dependency reads the client address or sets a cookie; see
Request Object & Content Type.
Prefer named functions to lambdas as dependables: a lambda has no useful name in tracebacks and is harder to target with an override.
Dependencies in WebSockets
Depends works in @app.websocket(…) handlers; the dependency may declare a WebSocket parameter, Query(),
Cookie() and Header() values. The one difference is the error channel: there is no HTTP response once the
connection is being upgraded, so HTTPException is not the tool. Raise WebSocketException with a close code instead.
Browsers cannot set arbitrary headers on the WebSocket handshake, which is why a cookie or a query parameter token is
the usual carrier:
from typing import Annotated
from fastapi import (Cookie, Depends, FastAPI, Query, WebSocket,
WebSocketDisconnect, WebSocketException, status)
app = FastAPI()
async def get_token(
websocket: WebSocket,
session: Annotated[str | None, Cookie()] = None,
token: Annotated[str | None, Query()] = None,
):
if session is None and token is None:
raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
return session or token
@app.websocket("/ws/books")
async def ws_books(websocket: WebSocket, who: Annotated[str, Depends(get_token)]):
await websocket.accept()
await websocket.send_json({"hello": who})
await websocket.close()
Source: FastAPI: WebSockets
{'hello': 'abc'}
closed with 1008
Code 1008 (policy violation) is the conventional "not authorised" close. A token in the query string appears in access
logs, so prefer a short-lived ticket. Everything else about the protocol is on
WebSockets.
How Dependencies Appear in OpenAPI
Dependencies themselves are never listed in the OpenAPI document. What shows up is the union of their parameters, attached to each operation that uses them, plus their security requirements:
| In the dependency | In /openapi.json |
|---|---|
Query, path, header or cookie parameters |
Merged into the operation’s |
A body model or |
The operation’s |
A security object ( |
An entry in the operation’s |
A plain |
Also treated as a security requirement, because the class is detected by type. |
The dependency’s own return type |
Nothing. FastAPI does not validate or document it; only the handler’s return annotation / |
Decorator, router or global |
Their parameters and security entries, like any other dependency. |
An API-key gate on the decorator shows both effects, read back from the generated document:
from typing import Annotated
from fastapi import Depends, FastAPI, Security
from fastapi.security import APIKeyHeader
app = FastAPI()
api_key = APIKeyHeader(name="X-API-Key")
def check_key(key: Annotated[str, Security(api_key)]) -> str:
return key
@app.get("/books", dependencies=[Depends(check_key)])
def books():
return []
doc = TestClient(app).get("/openapi.json").json()
print(doc["paths"]["/books"]["get"]["security"])
print(doc["components"]["securitySchemes"])
print(doc["paths"]["/books"]["get"].get("parameters"))
Source: FastAPI: Security, First Steps
[{'APIKeyHeader': []}]
{'APIKeyHeader': {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key'}}
None
The operation has a security entry and a scheme in the components, and no parameters (the key is described by the scheme, not as a
parameter). The Swagger UI therefore offers Authorize instead of an input field; the same mechanics for OAuth2
are on OAuth2 Password Flow & JWT, and the schema in
general on OpenAPI & Interactive Docs.
Two consequences: a plain Header() gate (like the earlier X-Token) does show up as a required header parameter,
and a dependency that reads request.headers directly is invisible to the documentation and the client generators.
Overriding Dependencies in Tests
app.dependency_overrides is a dictionary that maps an original dependable to a replacement. For every lookup,
FastAPI checks it first, so anything in the tree, however deep, can be swapped without touching the code under test.
The replacement has its own signature: its parameters are read like any dependency’s, which is why an override can
declare parameters of its own, or none:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_clock() -> str:
return "real clock"
def get_user(clock: Annotated[str, Depends(get_clock)]) -> str:
return f"user@{clock}"
@app.get("/me")
def me(user: Annotated[str, Depends(get_user)]):
return {"user": user}
{'user': 'user@real clock'}
{'user': 'user@frozen clock'}
{'user': 'test-user'}
{'user': 'user@real clock'}
The first call uses the real chain; overriding the leaf get_clock changes get_user without touching it;
overriding get_user replaces the whole branch (and get_clock is no longer called); .clear() restores the
original. A router-included route is overridden through the application’s dictionary, the one on the FastAPI
object that serves it, as the key is the function object, not a route.
Rules and pitfalls:
-
The key must be the same object used in
Depends(…). An override keyed by a different but equal-looking function does nothing. This is why named, module-level dependencies and aliases defined once matter. -
Reset the dictionary after each test, or the override leaks to the next one. A pytest fixture with
yieldthat clears it is the standard pattern, which is how the testing page does it. -
Overriding a parameterized dependency requires the key to be the instance (
fiction), not the class. -
The override replaces what is returned, so authorization logic inside the replaced function is also bypassed. Test the real checker separately.
-
Overrides apply to yield dependencies too; an override that is a plain function skips the original setup and teardown.
The testing setup that goes with this (fixtures, TestClient as a context manager, async tests with ASGITransport) is on
Testing, and the SQLModel session override is on
SQL Databases with SQLModel. Overrides are for tests; do not
use them to switch behaviour in production (use settings, see
Settings, Lifespan & Background Tasks).
Choosing the Right Mechanism
| You need | Use |
|---|---|
A value in the handler computed from the request |
A dependency in the handler parameters. |
A check or side effect, no value |
|
The same value on every route of a group |
A router-level dependency, plus an |
Setup and teardown around the handler |
A dependency with |
A work item after the response |
|
Behaviour for every request, including routes that do not exist or the docs |
Middleware (Middleware, CORS & Proxies). |
Application-lifetime resources (pools, models) |
The lifespan handler and |
Different configuration of the same check |
A parameterized dependency. |
Pitfalls
-
Calling the dependency.
Depends(get_value())evaluates the function at import time and passes its result; FastAPI then raisesTypeError: 1 is not a callable objectwhile the route is being declared (executed on 0.142.2). Pass the function. -
Missing annotation on a dependency parameter. A parameter with neither annotation nor default is treated as a required
strquery parameter:needs_q(q)was reachable as?q=hiand listed as('q', 'query', True)in the schema. Annotate every parameter so the source is intentional. -
Depends()on a non-class annotation.Annotated[dict, Depends()]raises at declaration time; use the explicit form. -
Blocking in an
async defdependency. Stalls the event loop. Use adefdependency or an async client. -
Expecting a dependency to run once per application. It runs per request; use the lifespan for shared resources.
-
Relying on order. Order is as documented above for the same request, but a sibling’s validation error can come after other dependencies already ran. Do not make dependencies assume the request will succeed.
-
Putting authorization in middleware or a global dependency only. A global dependency does not cover the docs routes or mounted sub-applications; mounted apps (see Sub-Applications, WSGI & GraphQL) have their own dependency tree.
-
Forgetting the cache. A dependency with a side effect is "called once" because of the cache, which is a property of the request, not a guarantee of your own design.
-
Shared mutable state in
__call__instances across concurrent requests. -
Creating dependables per request defeats caching and overriding.
-
Circular dependencies. A depends on B depends on A cannot be resolved; restructure by extracting the shared piece into a third dependency.
-
Heavy work in a global dependency. It is paid on every request, including cheap ones such as health checks.
-
Hiding the type.
→ dictorAnyas the dependency’s return annotation loses the editor support that is the main benefit of the pattern. Return a typed object.
Where Older Guides Are Outdated, and What Changed Recently
| Older guides and code | Today |
|---|---|
|
|
Repeating |
Reusable aliases, |
|
|
Tests with monkeypatching or global test settings |
|
|
Accepted since 0.123.5 (2025-12-02). |
Yield dependencies cleaning up before the response is sent, or unusable in background tasks (0.106 to 0.117) |
From 0.118.0 (2025-09-29) the exit code runs after the response, and a |
Security classes returning |
They return |
Optional extras installed with plain |
|
Details of other dated changes are in What’s Changed & Migration.