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 get_current_user that raises 401 before the handler runs; the handler just receives a User.

Business preconditions (the book exists, the caller owns it)

A dependency that loads or checks and raises HTTPException; the handler receives the verified object.

Testing without a real database or identity provider

app.dependency_overrides swaps any dependency for a fake, with no monkeypatching.

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}

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

def function

Resolve a header, load a row, build a client

Run in a worker thread so a blocking call does not stop the event loop.

async def function

Anything that can await

Runs on the event loop; never block in it. See Async & Concurrency.

Generator / async generator (yield)

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: Pagination(skip=…​, limit=…​).

Instance with __call__

Configurable ("parameterized") dependency

See Parameterized Dependencies.

functools.partial

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}
{'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

p: Annotated[Pagination, Depends()]

p: Annotated[Pagination, Depends(Pagination)]

p: Annotated[Pagination, Depends(Pagination)]

the explicit form; required when the callable is not the annotation, e.g. Annotated[dict, Depends(common_parameters)]

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.

flowchart TD H["path operation
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")]
Figure 1. The Bookshelf API dependency tree for POST /books/\{id}/reviews

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}

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_user authenticates (who is it?), get_current_active_user checks state (is the account usable?), require_scopes authorizes (may they do this?). Splitting them lets a public-but-personalised endpoint use only the first and an admin endpoint use all three.

  • Raise HTTPException in the dependency, not in the handler, when the failure is generic (401, 403, 404 for 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 with functools.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 scope option, 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=False or 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

p: Annotated[T, Depends(f)]

That path operation, and you receive the value.

Path operation decorator

@router.get("/", dependencies=[Depends(f)])

That path operation only; the return value is discarded.

Router

APIRouter(prefix="/books", dependencies=[Depends(f)])

Every operation of the router.

include_router

app.include_router(router, dependencies=[Depends(f)])

Every operation of that router, for this inclusion only (the same router can be mounted twice with different rules).

Application

FastAPI(dependencies=[Depends(f)])

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_token returns 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 400 came from verify_token, and later dependencies and the handler did not run.

  • Missing inputs. The gate’s own parameters (X-Token header) are validated like any other; the third request without the header produced a 422 instead of the 400, and, as in the earlier example, dependencies that did not need that header had already run (the log shows router, decorator and parameter dependency).

  • Global dependencies do not run for the docs. /docs, /redoc and /openapi.json are 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-Token header 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.

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
{'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") and require_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.

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
{'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()
{'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 parameters, deduplicated by name and location; constraints become schema keywords (maximum, minLength, …​).

A body model or Form() fields

The operation’s requestBody.

A security object (OAuth2PasswordBearer, APIKeyHeader, HTTPBearer, …​) used with Security() or as a default

An entry in the operation’s security plus a scheme in components.securitySchemes; the Swagger UI "Authorize" button appears.

A plain Depends() on a security-class instance

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 / response_model counts.

Decorator, router or global dependencies=[…​]

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"))
[{'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 yield that 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

dependencies=[…​] on the decorator or router.

The same value on every route of a group

A router-level dependency, plus an Annotated alias where the value is needed.

Setup and teardown around the handler

A dependency with yield (Dependencies with yield).

A work item after the response

BackgroundTasks, not a dependency (see Settings, Lifespan & Background Tasks).

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 app.state.

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 raises TypeError: 1 is not a callable object while 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 str query parameter: needs_q(q) was reachable as ?q=hi and 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 def dependency. Stalls the event loop. Use a def dependency 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. → dict or Any as 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

commons: dict = Depends(common_parameters) (default value style, as in older tutorials)

commons: Annotated[dict, Depends(common_parameters)]. Annotated is supported and recommended since 0.95.0; a default value after Depends is the old style you will still meet.

Repeating Depends(get_session) in every signature

Reusable aliases, SessionDep = Annotated[Session, Depends(get_session)].

Depends of a class repeated as Depends(Pagination)

Annotated[Pagination, Depends()]; both still work.

Tests with monkeypatching or global test settings

app.dependency_overrides, cleared after each test.

functools.partial not accepted as a dependable

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 scope option exists since 0.121.0; the details and the diagram are on Dependencies with yield.

Security classes returning 403 for missing credentials

They return 401 since 0.122.0 (2025-11-24); see Security Fundamentals.

Optional extras installed with plain pip install fastapi

pip install "fastapi[standard]" since 0.112.0.

Details of other dated changes are in What’s Changed & Migration.