Dependencies that depend on dependencies, and the tree FastAPI resolves before your handler runs.
Overview
The same syntax, one level down
A sub-dependency is a dependency that itself depends on something else. For example, a function that returns the current user might need the token from the header, and a function that checks admin rights might need the current user. Each piece asks for what it needs, and the framework builds the chain.
This lets you assemble behavior from small, reusable parts. Instead of one large function that reads a token, loads a user, and checks permissions, you write three small ones that each do a single job. Each can be tested alone and reused in different combinations.
This article starts with the syntax, which is the same as for any dependency, just applied one level down. Nothing new needs learning there. It then explains how the cache covers the whole tree. If two branches both need the same dependency, it runs once per request, and both receive the same value.
The next section covers failures. When a dependency deep in the chain raises an error, the whole request stops at that point, and nothing after it runs. This is what makes authentication checks reliable, since the handler cannot be reached by accident.
The article ends with a practical use: layering permissions. You will build up from a logged-in user, to an active user, to an admin, with each level adding one rule on top of the last. It is a clean way to express access control.
A useful way to think about it is as a recipe. The final dish needs a prepared ingredient, and that ingredient needs its own preparation. You list each step once, and FastAPI works out the order. This is also where mistakes in design show up quickly. If a chain becomes hard to follow, that is a sign to split it differently, and the article gives guidance on spotting this early. Each section is short and ends with a habit you can apply straight away, so you finish with working knowledge of how chains are built.
A dependency is a function whose params are request params. And Depends is a request param. So a dependency can depend on another:
There's no new method here. FastAPI walks the tree from the handler's signature down, calls each function in dependency order, and passes the results up.
Worth knowing
A dependency declares its own dependencies with the same syntax. FastAPI resolves the whole tree before calling the handler.
The per-request cache covers the tree, so a shared node reached by several paths is still called once.
A raise anywhere in the tree short-circuits everything below and after it, including the handler.
Layered permission checks are the natural use: each level adds one rule and an endpoint declares the level it needs.
Every parameter declared anywhere in the tree becomes the endpoint's parameter, and appears in the documentation.
Keep the tree shallow. Three levels reads well; five means the signature no longer tells a reader what the endpoint needs.
Sub-dependencies
Dependencies that depend on dependencies, and the tree resolved before your handler runs.
A dependency can declare its own
Exactly the same syntax, one level down. FastAPI resolves the whole tree before the handler is called.
example_01.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
USERS = {"tok-ada": {"name": "ada", "role": "admin"}}
def token(authorization: str = Header(default="")):
return authorization.replace("Bearer ", "")
def current_user(tok: str = Depends(token)):
if tok not in USERS:
raise HTTPException(401, "Sign in first")
return USERS[tok]
def admin_only(user: dict = Depends(current_user)):
if user["role"] != "admin":
raise HTTPException(403, "Admins only")
return user
@app.delete("/modules/{i}")
def remove(i: int, user: dict = Depends(admin_only)):
return {"deleted": i, "by": user["name"]}
c = TestClient(app)
print(c.request("DELETE", "/modules/7",
headers={"authorization": "Bearer tok-ada"}).json())
print("no token:", c.request("DELETE", "/modules/7").status_code)
Output
The tree is resolved once
A shared sub-dependency deep in the tree is still called a single time per request.
example_02.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
calls = []
def base():
calls.append("base")
return "B"
def left(b: str = Depends(base)): return "L(%s)" % b
def right(b: str = Depends(base)): return "R(%s)" % b
@app.get("/x")
def x(l: str = Depends(left), r: str = Depends(right), b: str = Depends(base)):
return {"l": l, "r": r, "b": b, "base_calls": len(calls)}
print(TestClient(app).get("/x").json())
print()
print("Three paths reach base; it ran once. That is why the cache matters.")
Output
Failures short-circuit the tree
If a sub-dependency raises, nothing below it or after it runs — including the handler.
example_03.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
ran = []
def token(authorization: str = Header(default="")):
ran.append("token")
tok = authorization.replace("Bearer ", "")
if not tok:
raise HTTPException(401, "No token")
return tok
def profile(tok: str = Depends(token)):
ran.append("profile")
return {"tok": tok}
@app.get("/me")
def me(p: dict = Depends(profile)):
ran.append("handler")
return p
c = TestClient(app)
r = c.get("/me")
print("no token:", r.status_code, "| ran:", ran)
ran.clear()
r = c.request("GET", "/me", headers={"authorization": "Bearer abc"})
print("token :", r.status_code, "| ran:", ran)
Output
Layering permissions
The natural use: each level adds one check, and an endpoint declares the level it needs.
example_04.pyFastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
USERS = {"a": {"name": "ada", "role": "admin"},
"r": {"name": "rex", "role": "reader"}}
def current_user(x_token: str = Header(default="")):
if x_token not in USERS:
raise HTTPException(401, "Sign in")
return USERS[x_token]
def verified(user: dict = Depends(current_user)):
return user # a real one would check a flag
def admin(user: dict = Depends(verified)):
if user["role"] != "admin":
raise HTTPException(403, "Admins only")
return user
@app.get("/public")
def public(): return {"level": "public"}
@app.get("/account")
def account(u: dict = Depends(verified)): return {"level": "verified", "you": u["name"]}
@app.get("/admin")
def admin_page(u: dict = Depends(admin)): return {"level": "admin", "you": u["name"]}
c = TestClient(app)
for path in ["/public", "/account", "/admin"]:
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
The pattern this shape is built for:
current_user — who is this? 401 if unknown. verified — are they confirmed? 403 if not. admin — are they an admin? 403 if not.
Each depends on the previous and adds just one rule. An endpoint then declares the level it needs. The level is visible in its signature.
The option — one check_permissions(level="admin") function with branching inside — works and hides the rules in a body rather than showing them in a chain. The layered version is easier to read and much easier to extend. Adding a level is a new function rather than a new branch.
Parameters gather from the whole tree
Every parameter any dependency declares becomes the endpoint's, and every one is documented.
example_05.pyFastAPI
from fastapi import Depends, FastAPI, Header, Query
app = FastAPI()
def paging(limit: int = Query(default=10, le=100)):
return limit
def locale(x_locale: str = Header(default="en")):
return x_locale
def context(limit: int = Depends(paging), loc: str = Depends(locale),
q: str = Query(default="")):
return {"limit": limit, "locale": loc, "q": q}
@app.get("/search")
def search(ctx: dict = Depends(context)):
return ctx
c = TestClient(app)
print(c.request("GET", "/search?q=vectors&limit=5",
headers={"x-locale": "fr"}).json())
print()
for p in app.openapi()["paths"]["/search"]["get"]["parameters"]:
print(" %-9s in %s" % (p["name"], p["in"]))
Output
Depth worth keeping
Three levels reads well. Beyond that the endpoint's signature stops telling you what it actually needs.
example_06.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
trace = []
def a(): trace.append("a"); return "a"
def b(x: str = Depends(a)): trace.append("b"); return x + "b"
def c_(x: str = Depends(b)): trace.append("c"); return x + "c"
def d(x: str = Depends(c_)): trace.append("d"); return x + "d"
def e(x: str = Depends(d)): trace.append("e"); return x + "e"
@app.get("/deep")
def deep(v: str = Depends(e)):
return {"value": v, "order": trace}
print(TestClient(app).get("/deep").json())
print()
print("It works. But 'deep' declares one parameter and needs five functions,")
print("and nothing in its signature says so.")
Output
The cache covers the tree
This is where per-request caching stops being an tuning and turns load-bearing.
In a realistic use, current_user is depended on by permissions. This Is depended on by admin_only. The handler may declare two of those right. Without caching the token would be decoded three or four times per request.
With it, once. The second editor above makes that visible: three split paths reach base. It runs a single time.
Failures short-circuit
If any dependency raises, all below and after it's skipped, including the handler.
That is what makes layered checks safe. admin_only can assume current_user succeeded. It only runs if it did. There's no need to check for None or re-verify. The tree promises sorting.
It's also why authentication as a dependency is truly safer than a check in the handler. A handler can forget to check. A signature can't. If the endpoint declares admin_only, the check ran.
Parameters gather upward
each param declared anywhere in the tree turns a param of the endpoint.
If paging declares limit, locale declares an x-locale header. context declares q and depends on both, then an endpoint based on context accepts all three. Checked, converted, and documented.
That is worth appreciating: the abstraction isn't opaque. A caller reading your OpenAPI doc sees each param the endpoint really takes. Deep in the tree it was declared. Nothing is hidden by the indirection.
The corollary is that a dependency adding a param changes the public contract of each endpoint using it. Adding a *needed* one is a breaking change for all of them at once. This Is worth remembering before doing it casually.
How deep to go
Three levels is comfortable and common. Five isn't.
The problem at depth isn't correctness. The last editor above works fine. It's that the endpoint's signature stops being helpful. def deep(v. Str = Depends(e)) tells a reader nothing about the five functions that must succeed, the params they collectively declare, or the errors they can raise.
Two habits keep it readable.
Name for the promise, not the method.admin_only says what the endpoint gets. check_role_after_verifying_token shows plumbing.
Flatten when a chain has no branch. If a is only ever used by b and b only by c, the three may be one function with a clear name. The chain earns its keep when levels are reused on its own.
Debugging one
When a dependency doesn't behave as expected, the handy question is *sorting*. What ran before it. Did anything above it raise?
A print at the top of each function shows the detail order at once. The order is fixed. There's no concurrency within one request's tree.
The other common surprise is caching: a dependency that appears to run once when you expected twice is the cache doing its job, and use_cache=False is the switch.
Mistakes people make with Sub-dependencies
Building a deep chain because it composes. It does compose. Five levels means the endpoint's signature no longer says what it needs. Three is comfortable.
Naming for the method.check_role_after_verifying_token shows plumbing. admin_only shows the promise the handler receives.
Flattening nothing. If a is only used by b and b only by c, they're one function with a clear name. A chain earns its keep when the levels are reused on its own.
Adding a needed param to a shared dependency. It turns needed on each endpoint in the tree at once - a breaking change for all of them, made in one line that mentions none of them.
given order without checking. The detail order is fixed. A print at the top of each function shows it in seconds. That is faster than thinking about it.
Losing the cache when debugging. A dependency that appears to run once when you expected twice is the cache working. use_cache=False is the switch.
What the tree is really for
The layered-permission shape is the case that justifies the feature.
Each level answers one question and can assume the previous one passed. A raise short-circuits all after it. admin never has to check whether current_user returned None, because if it had raised, admin wouldn't be running.
That promise is what makes it safe to put security in the dependency tree rather than in handlers. A handler can forget a check. A signature can't. If the endpoint declares admin, the whole chain above it ran and passed.
Reading a tree you did not write
Arriving at an unseen code, the dependency tree is one of the fastest ways to understand what the endpoints assume.
Start at a handler's signature and follow the names down. Each level tells you one need. The leaves are where the use touches the outside world - a header, a DB, a clock, a config value.
Two things that reading reveals quickly. Whether security is enforced always. If half the endpoints declare current_user and half read the header themselves, the second half are where the bugs are. And where the boundaries are: the leaves of the tree are the places to fake in tests. If there are many, the use is entangled with more of the world than it needs.
Cost
each dependency in the tree is a function call per request. The tree is resolved before the handler runs.
For the plain case - a handful of small functions - the cost is nothing next to a single query. It turns visible in two situations. A dependency doing real work, such as a lookup, that is now times'd across each endpoint sharing it. And a very wide tree where the sheer number of calls adds up under load.
Neither is a reason to avoid the feature. Both are reasons to know what is in the tree. A slow dependency near the root is slow for all. Its cost doesn't appear in any single endpoint's code.
A worked permission chain
Written out in full, because this is the shape most uses settle on.
def bearer_token(authorization: str = Header(default="")) -> str:
if not authorization.startswith("Bearer "):
raise HTTPException(401, "Missing bearer token")
return authorization[7:]
def current_user(token: str = Depends(bearer_token)) -> User:
user = decode(token)
if user is None:
raise HTTPException(401, "Invalid token")
return user
def active_user(user: User = Depends(current_user)) -> User:
if user.disabled:
raise HTTPException(403, "Account disabled")
return user
def admin(user: User = Depends(active_user)) -> User:
if not user.is_admin:
raise HTTPException(403, "Admins only")
return user
Four functions, four rules, each one line of logic. An endpoint declares the level it needs and gets all below it.
The properties worth noticing: each level has one reason to fail and one status. The token is decoded once but many levels are involved. And adding a rule - two-factor verified, subscription active - is a new function in the chain rather than a new branch inside an existing one.
That is what the tree is for. Not composition for its own sake. A set of needs that can be stated apart, reused on its own. Read from an endpoint's signature.
Cost and shape
each node in the tree is a function call per request, resolved before the handler runs.
For a handful of small functions that is nothing beside a single query. It turns visible in two situations worth watching. A dependency that does real work sitting near the root. Here Its cost is times'd by each endpoint below it. And a very wide tree under load. Here The call count itself adds up.
Neither argues against the feature. Both argue for knowing what is in the tree. A slow dependency high up is slow for all and its cost appears in no single endpoint's code.
Reading an unfamiliar one
The tree is also the fastest way into a code you didn't write.
Start at a handler's signature and follow the names down. Each level names one need. The leaves are where the use touches the outside world.
Two things that reading reveals at once. Whether security is applied always - if half the endpoints declare current_user and half read the header themselves, the second half is where the bugs live. And how entangled the use is - a wide set of leaves means many things must exist for any endpoint to run.
Sub-dependencies: summary
A dependency declares its own dependencies with the same syntax. FastAPI resolves the tree before the handler runs. The per-request cache covers the whole tree. A shared node is called once but many paths reach it.
A raise anywhere short-circuits all after it. This Is what lets each level assume the previous one passed - and what makes declaring a check in a signature safer than remembering it in a handler.
each param declared anywhere gathers upward into the endpoint's documented contract. Keep the tree about three levels deep. Name each level for the promise it gives.
Summary, in one line
Dependencies compose with the same syntax, the tree resolves before the handler, a shared node runs once, a raise stops all after it. Each param anywhere in the tree turns part of the endpoint's public contract.
A closing thought
A dependency tree is a description of what an endpoint assumes, written in a form the framework enforces.
That is a stronger promise than docs and a cheaper one than tests. If the endpoint declares admin, then a request that reaches the handler came from an authenticated, active administrator - not because somebody remembered to check. Because the handler couldn't have run else.
Keeping the tree shallow and naming each level for its promise is what keeps that description readable.
Two rules
Name each level for what it promises, not for what it does. admin tells a reader what the handler receives. check_role_after_token shows the plumbing.
Stop at about three levels. The tree stays handy while a signature still implies what the endpoint needs. Stops being handy the moment it doesn't.
Questions people ask about Sub-dependencies
What is a sub-dependency? A dependency that itself declares Depends() in its parameters. FastAPI resolves the whole tree before calling your endpoint.
Is a shared dependency called twice in one tree? No. Results are cached per request, so a dependency used at two levels runs once.
What happens when a deep dependency raises? The chain stops there and the client gets that error. The dependencies above it and the endpoint never run.
How do I layer permissions? Build them as a chain, for example get_current_user, then get_active_user, then get_admin_user, each depending on the one before.
Where do query parameters from sub-dependencies appear? They are gathered upward, so the endpoint's OpenAPI schema lists parameters from every level of the tree.
How deep is too deep? When you cannot read the endpoint signature and know what runs. Two or three levels is common, beyond that consider flattening.
Where to go after Sub-dependencies
Attaching a dependency to a router or an use rather than a param. This Is how a section gets a rule that a new route can't escape.
A tree that reads well is one where each name is a noun or an adjective showing the caller - a user, an admin, an active account - rather than a verb showing the check. The handler is receiving a promise, not commissioning an inspection.
Check yourself
0 of 4
Answer without scrolling back up.
A sub-dependency is reached by three different paths in one request. How many times does it run?
In a real application `current_user` is reached several ways, and without the cache the token would be decoded repeatedly. This is where caching stops being an optimisation.
A dependency in the middle of the tree raises. What runs after it?
That short-circuit is what lets a later level assume an earlier one succeeded - and why declaring a check in the signature is safer than remembering it in a handler.
A dependency three levels down declares a `limit` query parameter. Does it appear in the docs?
Parameters gather upward, so the indirection hides nothing from the contract - and adding a required one is a breaking change for every endpoint using that dependency.
What is the problem with a five-level dependency chain?
It works correctly. But one parameter standing for five functions, their parameters and their possible errors is no longer informative.
Cheat sheet
Sub-dependencies
Chain dependencies that depend on other dependencies, follow the resolution order, and see FastAPI call each one correctly before your handler.
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.