Anything callable works, which is how a dependency gets configuration of its own.
Overview
Callables, not functions
A class dependency is a dependency written as a class instead of a function. The idea rests on a Python fact: anything you can call counts as a callable, and classes are callable. When you pass a class to Depends, FastAPI creates an instance for each request and passes its parameters from the request, just as it would for a function.
This matters when a dependency has configuration of its own. A pagination helper might need a default page size. A permission check might need to know which role to require. A class lets you set those options once and reuse the object in many endpoints, with each use configured differently.
This article starts with the idea that callables work, not only functions, and why that opens up more options. It then describes what a class buys you over a plain function, including grouped parameters and attributes you can read in the handler. A section follows on configured instances, where you create an object with settings and give that object to Depends.
Timing is the next topic. You will see when the construction happens, once per request or once per app, and why that affects what you can safely store inside. The article finishes with returning a model, which keeps the result clean and well described in the docs.
If your dependencies keep growing extra arguments, a class is often the neater answer.
You do not need to be an expert in object-oriented design to use this feature. The classes involved are small, usually just an initializer that stores a few values. The benefit is practical: less repeated code, clearer names in your handlers, and settings that live next to the logic that uses them. Along the way you will see how this approach compares with closures and with simple functions, so you can choose the lightest tool that does the job. Not every dependency needs to be a class. Many are better left as plain functions, and the article is clear about where the line falls. Each section is short and ends with a habit you can apply straight away, so you finish with working knowledge and a clear rule for choosing between the two forms.
Depends takes a callable. A function is the obvious one. A class is a callable too. Calling it makes an instance.
class Pagination:
def __init__(self, limit: int = 10, offset: int = 0):
self.limit = min(limit, 100)
self.offset = offset
def list_modules(page: Pagination = Depends(Pagination)):
...
FastAPI reads __init__'s signature just as it reads a function's: limit and offset become query params, checked and documented. The handler receives the instance.
Because the annotation and the callable are the same thing, there's a shorthand:
def list_modules(page: Pagination = Depends()):
Worth knowing
Any callable works. A class is a dependency because calling it constructs an instance, and FastAPI reads __init__'s signature for parameters.
Depends() with no argument uses the parameter's annotation as the callable — the shorthand for a class dependency.
Give a class __call__ and its instances become dependencies, which is how one class produces several configured rules.
The instance is built at import, so configuration is not per-request work; only __call__ runs each time.
A dependency can return a Pydantic model, which gives the result validation, constraints and a documented schema.
Prefer a function when there is no configuration and no state. A class without either is ceremony.
Class Dependencies
Anything callable works. This Is how a dependency gets config of its own.
A class with __init__ is a dependency
FastAPI reads __init__'s signature the way it reads a function's, and gives the handler the instance.
example_01.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
class Pagination:
def __init__(self, limit: int = 10, offset: int = 0):
self.limit = min(limit, 100)
self.offset = offset
def slice(self, rows):
return rows[self.offset:self.offset + self.limit]
@app.get("/modules")
def list_modules(page: Pagination = Depends(Pagination)):
rows = ["m%d" % i for i in range(20)]
return {"limit": page.limit, "offset": page.offset, "rows": page.slice(rows)}
c = TestClient(app)
print(c.get("/modules?limit=3").json())
print(c.get("/modules?limit=3&offset=5").json())
Output
The shorthand
Because the class is both the type and the callable, Depends() with no argument uses the annotation.
example_02.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
class Pagination:
def __init__(self, limit: int = 10, offset: int = 0):
self.limit, self.offset = limit, offset
@app.get("/long")
def long_form(page: Pagination = Depends(Pagination)):
return {"limit": page.limit}
@app.get("/short")
def short_form(page: Pagination = Depends()):
return {"limit": page.limit}
c = TestClient(app)
print("long :", c.get("/long?limit=4").json())
print("short:", c.get("/short?limit=4").json())
print()
print("Same thing. The annotation supplies the callable.")
Output
An instance is a configured dependency
Give the class a __call__ and instances become dependencies you can parameterise — one class, several rules.
example_03.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
USERS = {"a": "admin", "r": "reader"}
class RequireRole:
def __init__(self, role: str):
self.role = role
def __call__(self, x_token: str = Header(default="")):
role = USERS.get(x_token)
if role is None:
raise HTTPException(401, "Sign in")
if role != self.role:
raise HTTPException(403, "Needs role %r" % self.role)
return role
require_admin = RequireRole("admin")
require_reader = RequireRole("reader")
@app.get("/admin")
def admin(role: str = Depends(require_admin)): return {"role": role}
@app.get("/reading")
def reading(role: str = Depends(require_reader)): return {"role": role}
c = TestClient(app)
for path in ["/admin", "/reading"]:
for tok in ["", "r", "a"]:
h = {"x-token": tok} if tok else {}
print("%-9s tok=%-2s -> %s" % (path, tok or "-",
c.request("GET", path, headers=h).status_code))
Output
A model as a dependency
The class can be a Pydantic model, which gives the filters validation, constraints and a documented schema.
example_04.pyFastAPI
import json
from typing import Literal
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class Filters(BaseModel):
q: str = Field(default="", max_length=50)
limit: int = Field(default=10, ge=1, le=100)
sort: Literal["title", "minutes"] = "title"
def filters(
q: str = Query(default="", max_length=50),
limit: int = Query(default=10, ge=1, le=100),
sort: Literal["title", "minutes"] = "title",
) -> Filters:
return Filters(q=q, limit=limit, sort=sort)
@app.get("/modules")
def list_modules(f: Filters = Depends(filters)):
return f.model_dump()
c = TestClient(app)
print(c.get("/modules?q=vec&limit=5&sort=minutes").json())
print("bad sort:", c.get("/modules?sort=colour").status_code)
print()
for p in app.openapi()["paths"]["/modules"]["get"]["parameters"]:
print(" %-6s %s" % (p["name"], json.dumps(p["schema"])[:60]))
Output
Configuration decided once
The instance is built at import, so its configuration is not per-request work — only __call__ runs each time.
example_05.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
built = []
class RateLimit:
def __init__(self, per_minute: int):
built.append(per_minute) # runs once, at import
self.per_minute = per_minute
self.seen = 0
def __call__(self):
self.seen += 1
return {"limit": self.per_minute, "calls": self.seen}
tight = RateLimit(5)
loose = RateLimit(100)
@app.get("/tight")
def a(r: dict = Depends(tight)): return r
@app.get("/loose")
def b(r: dict = Depends(loose)): return r
c = TestClient(app)
print(c.get("/tight").json()); print(c.get("/tight").json())
print(c.get("/loose").json())
print()
print("constructed:", built, "- twice, at import, not per request")
Output
When a function is enough
A class earns its place when there is configuration or state. Without either, it is a function with extra ceremony.
example_06.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
# No configuration, no state - a function says it more plainly.
def pagination(limit: int = 10, offset: int = 0):
return {"limit": limit, "offset": offset}
class PaginationClass:
def __init__(self, limit: int = 10, offset: int = 0):
self.limit, self.offset = limit, offset
@app.get("/fn")
def fn(p: dict = Depends(pagination)): return p
@app.get("/cls")
def cls(p: PaginationClass = Depends()): return {"limit": p.limit, "offset": p.offset}
c = TestClient(app)
print("function:", c.get("/fn?limit=3").json())
print("class :", c.get("/cls?limit=3").json())
print()
print("Identical behaviour. Reach for the class when it holds something.")
Output
What a class buys
Two things a function can't easily give you.
Methods. The instance can carry action, not just data. page.slice(rows) keeps the pagination logic with the pagination params, rather than repeating the math in each handler.
config, via __call__.
Configured instances
This is the pattern worth knowing. It's the main reason to reach for a class:
Now Depends(require_admin) and Depends(require_reader) are two dependencies from one class. __init__ takes your config. __call__ takes the request params.
The option with plain functions is a factory returning a closure. This Works and reads less clearly:
def require_role(role):
def dep(x_token: str = Header(default="")):
...
return dep
Both are used in real code. The class version keeps the config visible as fields and gives you one spot to put related helpers.
When the construction happens
Worth being precise about, because it affects what belongs where.
RequireRole("admin") runs at import, once. __call__ runs per request.
So costly setup belongs in __init__. Compiling a regular formula, loading a rules table, reading config. Per-request work belongs in __call__.
The instance is shared across requests. This Means any state you keep on it's shared too. That is fine for config and a genuine hazard for anything mutable. A counter on the instance counts across all requests and all users. In a multi-worker deployment counts per worker. This Is almost never what someone wanted from a rate limiter.
If a dependency needs per-request state, return it from __call__ rather than storing it on self.
Returning a model
A dependency's return value can be anything. A Pydantic model is often the right choice for a group of filters:
The handler then gets field access, editor completion and a typed object rather than a dict. The params are still declared on their own. They still appear in the docs as query params. Which is what a caller needs to see.
There's a temptation to annotate the param with the model right and skip the function. Resist it. A model param means a request *body*. On a GET that isn't what you want.
Function or class?
The question that settles it: does it hold anything?
If the dependency is "read these params and hand them over", a function is plainer and shorter.
If it has config decided at import, methods worth keeping beside the data, or a family of related variants, a class earns its keep.
Most dependencies in most uses are functions. The class form is for the handful that are parameterised. Permissions, rate limits, feature gates. And it's worth knowing just so you recognise the shape when you need it.
The dependency turns a named type, the signature reads as plain Python. The need is stated in one place that each endpoint can reuse. For a code with a few permission levels this is the tidiest form on hand.
Mistakes people make with Class Dependencies
Keeping mutable state on the instance. It's shared across each request. In a multi-worker deployment it's per worker. A counter there counts a thing no one wanted. Per-request state belongs in what __call__ returns.
**costly work in __call__.** That runs per request. Compiling a pattern or loading a table belongs in __init__. This Runs once at import.
Annotating the param with a Pydantic model right. A model annotation means a request *body*. For query filters, declare the params in a function and make the model inside it.
A class with no config and no state. That is a function with extra ceremony. Reach for the class when it holds a thing.
**Losing __call__ and wondering why the instance isn't a dependency.** Depends(SomeClass) calls the class. Depends(some_instance) calls the instance. An instance is only callable if the class defines __call__.
Factory function or class?
Both give configured dependencies, and both appear in real code.
A closure factory is shorter for one small rule:
def require_role(role):
def dep(x_token: str = Header(default="")):
...
return dep
A class keeps the config visible as fields, gives related helpers one spot to live. Is easier to inspect in a debugger.
For one param and three lines, the closure. For a family of rules with shared helpers, the class.
A worked family
The shape that justifies the class form, written out once.
class RequireScope:
def __init__(self, *scopes):
self.scopes = set(scopes)
def __call__(self, user: User = Depends(current_user)):
missing = self.scopes - set(user.scopes)
if missing:
raise HTTPException(403, "Missing scope(s): %s" % ", ".join(sorted(missing)))
return user
read_modules = RequireScope("modules:read")
write_modules = RequireScope("modules:read", "modules:write")
One class, one rule, and as many configured dependencies as the use has permission levels. Each endpoint declares the one it needs, and the declaration is readable: Depends(write_modules) says what the endpoint needs without opening anything.
Adding a scope is a new module-level name. Changing how scopes are checked is one method. Neither touches an endpoint.
Instances are shared
Because read_modules is created at import, it's one object shared by each request that reaches an endpoint declaring it.
That is what makes it cheap. It's the constraint to respect. The instance may hold config, and it must not hold anything about a given request. If you find yourself assigning to self inside __call__, the value belongs in the return instead.
The same applies across workers. Each process has its own instance. Anything built up on self is per process rather than per use - which is why an instance field is the wrong place for a rate-limit counter. A shared store is the right one.
Where the instance lives
One more result of build happening at import. It decides where these belong in a project.
require_admin = RequireRole("admin") is a module-level name. It's created when the module is imported, shared by each request. Referenced by each endpoint that needs it. That makes dependencies.py its natural home, beside the plain functions.
Two hands-on effects follow.
Import order matters slightly. Anything the constructor reads - a setting, an setting var - must be on hand at import. If it isn't, the failure is at startup rather than at request time. This Is the better of the two. It means config has to be loaded before dependencies are imported.
Reloading in development recreates them. With --reload, saving a file rebuilds the instances and discards what they held. That is invisible for config and confusing for anything stateful. This Is one more arg for keeping state off self.
When the parameters differ per endpoint
A related pattern worth recognising: at times the config isn't fixed at import but supplied per route.
The class form handles it, because each endpoint can make its own:
Each decorator builds an instance at import, one per route. This Is just the same method with a shorter lifetime for the name. It reads well for a rule that varies by number across endpoints. Less well once the config is more than a value or two - at which point a named instance is clearer than an inline build.
The three forms side by side
All three give a configured dependency. Choosing between them is mostly about how much the config carries.
A plain function, when there's nothing to configure. def pagination(limit. Int = 10) is the whole thing. Any other form is ceremony around it.
A closure factory, when one small value varies. Short. The config is a captured local no one can inspect.
**A class with __call__**, when the config is worth naming, a few rules share helpers, or you want the instance to be inspectable. RequireScope("modules:write").scopes is readable in a debugger. A closure's captured var isn't.
The progression is worth following in that order. Start with the function, reach for the factory when a value varies. Reach for the class when the factory starts growing a second function beside it.
What a class must not do
One rule, stated plainly, because breaking it makes bugs that only appear under load.
The instance is created once and shared by each request in that worker. Anything written to self during __call__ is shared state across concurrent requests and split state across workers.
For config that is just right - it's read-only and same all over. For anything per-request it's wrong twice over: two requests interleave, and two workers disagree.
If __call__ needs to give a thing request-exact, it returns it. That is what the handler receives. It's the only value with the right lifetime.
Class Dependencies: summary
Depends takes any callable, so a class is a dependency: FastAPI reads __init__ for params and hands the handler the instance. Depends() with no arg uses the annotation.
Give the class __call__ and its instances become configured dependencies - one class, a family of rules, each declared by name at the endpoints that need it. __init__ runs once at import. __call__ runs per request. Nothing request-exact should be stored on self.
Reach for a function when there's no config, no state and no action to keep beside the data.
Summary, in one line
Use a function until the dependency has config. Then use a class whose __init__ takes the config and whose __call__ takes the request - remembering that the instance is built once and shared. Nothing about a single request may live on it.
A closing thought
The class form is the least-used part of this tier and the one worth recognising rather than reaching for.
Most dependencies are functions and should stay functions. But when an use grows a few variants of one rule - three permission levels, four rate limits, five feature gates - writing five near-same functions is worse than writing one class and five names.
The signal is duplication with one value changed. That is what __init__ is for.
Two rules
**config in __init__, request handling in __call__.** The first runs once at import. The second runs per request.
**Nothing about a single request on self.** The instance is shared across each concurrent request in the worker and duplicated across workers. Anything stored there's both a race and a lie. Return it instead.
Where it sits in the tier
This is the smallest module of the five, and on purpose so.
Function dependencies cover the overwhelming majority of real use. Sub-dependencies handle composition. yield handles lifetime. Router-level handles scope. replaces handle testing.
The class form fills one exact gap: a rule that is the same shape at a few settings. Recognising that gap - and not reaching for a class before you're in it - is the whole lesson.
Questions people ask about Class Dependencies
Why would a class be a dependency? A class is callable, so Depends(MyClass) calls its constructor with parameters read from the request. You get an object with attributes.
Do I need to write Depends(MyClass()) or Depends(MyClass)? Write Depends(MyClass) to have FastAPI construct it per request. Passing an instance only works if the instance defines __call__.
How do I create a configurable dependency? Make a class with __init__ for configuration and __call__ for the per-request logic, then pass an instance such as Depends(RoleChecker("admin")).
Should I use a function or a class? Use a function for simple logic, and a class when you want grouped parameters, shared defaults, or configuration.
Are class instances shared between requests? A class used as Depends(MyClass) gets a new instance per request. An instance you create yourself at import time is shared, so keep it stateless.
Can a class dependency be a Pydantic model? Yes, but a model is read as a body unless you mark it with Depends or Query. Use Depends to read its fields from the query string.
Where to go after Class Dependencies
Applying a dependency to a whole router or the entire use. That a section is protected without each endpoint repeating the declaration - and without a route added later quietly missing it.
Check yourself
0 of 4
Answer without scrolling back up.
Why is a class a valid dependency?
FastAPI reads `__init__`'s signature the way it reads a function's, so its parameters become request parameters and the handler receives the instance.
What does `Depends()` with no argument use?
For a class dependency the annotation and the callable are the same thing, so the argument is redundant.
When does `RequireRole("admin")` run?
Configuration belongs in `__init__` and per-request work in `__call__`. The instance is shared across requests, so mutable state on `self` is shared too.
When is a plain function the better choice?
Most dependencies just read parameters and hand them over. A class without configuration or behaviour is the same thing with more ceremony.
Cheat sheet
Class Dependencies
Turn a class with __init__ parameters into a FastAPI dependency and see how any callable with the right signature becomes injectable config.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.