Module 1: Dependency Injection

Sub-dependencies and Composition — Dependency Chains

Capsule overview

In the previous capsule you created individual dependencies: pagination_params, get_task_or_404, filters. Each one solves a specific problem. But in a real API, dependencies combine: you need to verify that the user is authenticated and that the task exists and that the user has permission over that task. That requires one dependency to use the result of another dependency.

FastAPI supports this natively: a dependency function can declare Depends() in its own signature. FastAPI resolves the whole chain automatically, running the dependencies in the right order. This is called sub-dependencies — a dependency that depends on another.

You'll also see class-based dependencies: classes with a __call__ method that act as configurable dependencies. Instead of creating 5 pagination functions with different limits, you create one Paginator(max_limit=50) class that behaves like a function.

Combining sub-dependencies and class-based dependencies gives you a powerful composition system: small pieces that snap together like LEGO to build complex abstractions.


Sub-dependencies: a dependency that uses another

A sub-dependency is a dependency function that has its own Depends():

from fastapi import FastAPI, Depends, HTTPException, Header

app = FastAPI()

users_db = {
    "token-alice": {"id": 1, "name": "Alice", "role": "admin"},
    "token-bob": {"id": 2, "name": "Bob", "role": "user"},
}

tasks = [
    {"id": 1, "title": "Deploy API", "owner_id": 1},
    {"id": 2, "title": "Write docs", "owner_id": 2},
    {"id": 3, "title": "Fix bug", "owner_id": 1},
]


def get_current_user(authorization: str = Header(...)) -> dict:
    user = users_db.get(authorization)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user


def get_task_or_404(task_id: int) -> dict:
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


def get_own_task(
    current_user: dict = Depends(get_current_user),
    task: dict = Depends(get_task_or_404),
) -> dict:
    if task["owner_id"] != current_user["id"]:
        raise HTTPException(status_code=403, detail="Not your task")
    return task


@app.get("/tasks/{task_id}")
def get_task(task: dict = Depends(get_own_task)):
    return task

The resolution flow

Request: GET /tasks/2 with header Authorization: token-bob
    ↓
FastAPI sees Depends(get_own_task) in get_task
    ↓
It reads get_own_task's signature → it needs:
    - current_user = Depends(get_current_user)
    - task = Depends(get_task_or_404)
    ↓
Runs get_current_user(authorization="token-bob")
    → Returns {"id": 2, "name": "Bob", "role": "user"}
    ↓
Runs get_task_or_404(task_id=2)
    → Returns {"id": 2, "title": "Write docs", "owner_id": 2}
    ↓
Runs get_own_task(current_user={...}, task={...})
    → owner_id == current_user["id"] ✅
    → Returns the task
    ↓
Runs get_task(task={...})
    → Response: {"id": 2, "title": "Write docs", "owner_id": 2}
Request: GET /tasks/1 with header Authorization: token-bob
    ↓
get_current_user → Bob (id: 2)
get_task_or_404 → Task 1 (owner_id: 1)
get_own_task → 1 != 2 → HTTPException 403 "Not your task"
    ↓
get_task NEVER runs

Chain depth

Chains can go as deep as you need:

def get_db():
    return {"connection": "active"}

def get_current_user(
    db: dict = Depends(get_db),
    authorization: str = Header(...),
) -> dict:
    # uses db to look up the user by token
    ...

def get_admin_user(
    user: dict = Depends(get_current_user),
) -> dict:
    if user["role"] != "admin":
        raise HTTPException(status_code=403, detail="Admin only")
    return user

@app.delete("/admin/tasks/{task_id}")
def admin_delete(admin: dict = Depends(get_admin_user)):
    ...
get_db ← get_current_user ← get_admin_user ← admin_delete
  (L3)        (L2)              (L1)          (endpoint)

FastAPI resolves from the deepest dependency (L3) up to the endpoint.


Composing dependencies

Composition is the pattern of combining small dependencies to build larger abstractions. Instead of writing one mega-function that does everything, you compose pieces:

from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, Query

app = FastAPI()

tasks = [
    {"id": 1, "title": "Buy groceries", "completed": False, "priority": "medium", "category": "personal"},
    {"id": 2, "title": "Deploy API", "completed": False, "priority": "high", "category": "work"},
    {"id": 3, "title": "Study Python", "completed": True, "priority": "high", "category": "study"},
    {"id": 4, "title": "Work out", "completed": True, "priority": "low", "category": "personal"},
    {"id": 5, "title": "Code review", "completed": False, "priority": "medium", "category": "work"},
]


def pagination_params(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=10, ge=1, le=100),
) -> dict:
    return {"skip": skip, "limit": limit}


def filter_params(
    completed: Optional[bool] = None,
    priority: Optional[str] = Query(default=None, pattern="^(high|medium|low)$"),
    category: Optional[str] = None,
    search: Optional[str] = Query(default=None, min_length=1),
) -> dict:
    return {
        "completed": completed,
        "priority": priority,
        "category": category,
        "search": search,
    }


def sorted_filtered_tasks(
    filters: dict = Depends(filter_params),
) -> list:
    result = tasks[:]
    if filters["completed"] is not None:
        result = [t for t in result if t["completed"] == filters["completed"]]
    if filters["priority"]:
        result = [t for t in result if t["priority"] == filters["priority"]]
    if filters["category"]:
        result = [t for t in result if t["category"] == filters["category"]]
    if filters["search"]:
        term = filters["search"].lower()
        result = [t for t in result if term in t["title"].lower()]
    return result


@app.get("/tasks")
def list_tasks(
    filtered: list = Depends(sorted_filtered_tasks),
    pagination: dict = Depends(pagination_params),
):
    start = pagination["skip"]
    end = start + pagination["limit"]
    return {
        "total": len(filtered),
        "tasks": filtered[start:end],
    }


@app.get("/tasks/stats")
def task_stats(filtered: list = Depends(sorted_filtered_tasks)):
    completed = sum(1 for t in filtered if t["completed"])
    return {
        "total": len(filtered),
        "completed": completed,
        "pending": len(filtered) - completed,
    }

sorted_filtered_tasks depends on filter_params. Both endpoints (list_tasks and task_stats) reuse sorted_filtered_tasks. If you add a new filter, you do it in exactly one place.


Class-based dependencies

When you need a configurable dependency — one that behaves differently depending on parameters — classes are the answer. A class with __call__ acts as a function that FastAPI can use with Depends():

The basic pattern

from fastapi import FastAPI, Depends, Query

app = FastAPI()

items = [{"id": i, "name": f"Item {i}"} for i in range(1, 51)]


class Paginator:
    def __init__(self, max_limit: int = 100):
        self.max_limit = max_limit

    def __call__(
        self,
        skip: int = Query(default=0, ge=0),
        limit: int = Query(default=10, ge=1),
    ) -> dict:
        effective_limit = min(limit, self.max_limit)
        return {"skip": skip, "limit": effective_limit}


paginator_default = Paginator()
paginator_small = Paginator(max_limit=20)


@app.get("/items")
def list_items(pagination: dict = Depends(paginator_default)):
    start = pagination["skip"]
    return items[start : start + pagination["limit"]]


@app.get("/admin/items")
def admin_list_items(pagination: dict = Depends(paginator_small)):
    start = pagination["skip"]
    return items[start : start + pagination["limit"]]

Paginator is a class that:

  1. Gets configured in __init__ (e.g. max_limit=20)
  2. Acts as a function in __call__ (takes query params, returns a result)

paginator_default allows up to 100 items. paginator_small caps at 20. The endpoint only says Depends(paginator_small) — the configuration is encapsulated.

A class-based dependency for verification

from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()


class RoleChecker:
    def __init__(self, required_role: str):
        self.required_role = required_role

    def __call__(self, x_user_role: str = Header(...)):
        if x_user_role != self.required_role:
            raise HTTPException(
                status_code=403,
                detail=f"Role '{self.required_role}' required, got '{x_user_role}'",
            )
        return x_user_role


require_admin = RoleChecker("admin")
require_editor = RoleChecker("editor")


@app.get("/admin/dashboard", dependencies=[Depends(require_admin)])
def admin_dashboard():
    return {"message": "Admin dashboard"}


@app.post("/articles", dependencies=[Depends(require_editor)])
def create_article():
    return {"message": "Article created"}
curl http://127.0.0.1:8000/admin/dashboard -H "X-User-Role: admin"
# {"message":"Admin dashboard"}

curl http://127.0.0.1:8000/admin/dashboard -H "X-User-Role: user"
# {"detail":"Role 'admin' required, got 'user'"}

curl -X POST http://127.0.0.1:8000/articles -H "X-User-Role: editor"
# {"message":"Article created"}

Function or class? When to use each

CriterionFunctionClass
No configuration✅ Use a functionUnnecessary
With configuration (params)Possible but awkward✅ Use a class
State between requestsDoesn't apply✅ Use a class
Simplicity✅ SimplerMore code
Reusable with variantsYou'd need closures✅ Different instances

The rule: if you need the same logic with different configurations, use a class. If not, a function is simpler.


An advanced example: a composed permission system

Combining sub-dependencies with class-based dependencies:

from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()

users_db = {
    "token-alice": {"id": 1, "name": "Alice", "role": "admin", "permissions": ["read", "write", "delete"]},
    "token-bob": {"id": 2, "name": "Bob", "role": "editor", "permissions": ["read", "write"]},
    "token-charlie": {"id": 3, "name": "Charlie", "role": "viewer", "permissions": ["read"]},
}

tasks = [
    {"id": 1, "title": "Deploy API", "owner_id": 1},
    {"id": 2, "title": "Write docs", "owner_id": 2},
]


def get_current_user(authorization: str = Header(...)) -> dict:
    user = users_db.get(authorization)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user


class PermissionChecker:
    def __init__(self, required_permission: str):
        self.required_permission = required_permission

    def __call__(self, user: dict = Depends(get_current_user)) -> dict:
        if self.required_permission not in user["permissions"]:
            raise HTTPException(
                status_code=403,
                detail=f"Permission '{self.required_permission}' required",
            )
        return user


require_read = PermissionChecker("read")
require_write = PermissionChecker("write")
require_delete = PermissionChecker("delete")


@app.get("/tasks")
def list_tasks(user: dict = Depends(require_read)):
    return {"user": user["name"], "tasks": tasks}


@app.post("/tasks", status_code=201)
def create_task(user: dict = Depends(require_write)):
    return {"user": user["name"], "message": "Task created"}


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int, user: dict = Depends(require_delete)):
    return {"user": user["name"], "message": f"Task {task_id} deleted"}
# Charlie (viewer) can read
curl http://127.0.0.1:8000/tasks -H "Authorization: token-charlie"
# {"user":"Charlie","tasks":[...]}

# Charlie CANNOT write
curl -X POST http://127.0.0.1:8000/tasks -H "Authorization: token-charlie"
# {"detail":"Permission 'write' required"}

# Alice (admin) can delete
curl -X DELETE http://127.0.0.1:8000/tasks/1 -H "Authorization: token-alice"
# {"user":"Alice","message":"Task 1 deleted"}

The chain is: Header → get_current_user → PermissionChecker.__call__ → endpoint. Each PermissionChecker instance checks a different permission, but they all reuse get_current_user.


Dependencies shared across endpoints

You can declare dependencies at the router or app level so they apply to every endpoint without repeating Depends() on each one:

from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()


def verify_api_key(x_api_key: str = Header(...)):
    if x_api_key != "valid-key":
        raise HTTPException(status_code=403, detail="Invalid API key")


# Every endpoint in the app requires an API key
app = FastAPI(dependencies=[Depends(verify_api_key)])


@app.get("/tasks")
def list_tasks():
    return [{"id": 1, "title": "Task 1"}]


@app.get("/users")
def list_users():
    return [{"id": 1, "name": "Alice"}]

Now verify_api_key runs for every endpoint. You don't need to add dependencies=[Depends(verify_api_key)] to each one.

This gets even more useful with APIRouter (Module 2), where you can apply dependencies to a specific group of endpoints.


Execution order and caching

Execution order

FastAPI runs dependencies "bottom-up" — the deepest ones first:

def dep_c():
    print("C ran")
    return "c"

def dep_b(c: str = Depends(dep_c)):
    print("B ran")
    return f"b+{c}"

def dep_a(b: str = Depends(dep_b)):
    print("A ran")
    return f"a+{b}"

@app.get("/test")
def test(a: str = Depends(dep_a)):
    return {"result": a}

Console output:

C ran
B ran
A ran

The result: {"result": "a+b+c"}.

Caching in chains

If two dependencies share a common sub-dependency, it only runs once:

def get_settings():
    print("get_settings ran")
    return {"debug": True}

def dep_x(settings: dict = Depends(get_settings)):
    return {"x": True, "debug": settings["debug"]}

def dep_y(settings: dict = Depends(get_settings)):
    return {"y": True, "debug": settings["debug"]}

@app.get("/test")
def test(
    x: dict = Depends(dep_x),
    y: dict = Depends(dep_y),
):
    return {"x": x, "y": y}

Console output: get_settings ran shows up exactly once. dep_x and dep_y receive the same result from get_settings.


Exercises

Exercise 1: A simple sub-dependency (Easy)

Create get_db() that returns a dict simulating a connection. Create get_user_from_db(db, user_id) as a sub-dependency that uses get_db(). Use get_user_from_db in GET /users/{user_id}. Check that the path parameter resolves correctly.

See solution
from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

fake_db = {
    1: {"id": 1, "name": "Alice", "email": "alice@test.com"},
    2: {"id": 2, "name": "Bob", "email": "bob@test.com"},
    3: {"id": 3, "name": "Charlie", "email": "charlie@test.com"},
}


def get_db() -> dict:
    return fake_db


def get_user_from_db(user_id: int, db: dict = Depends(get_db)) -> dict:
    user = db.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail=f"User {user_id} not found")
    return user


@app.get("/users/{user_id}")
def get_user(user: dict = Depends(get_user_from_db)):
    return user
curl http://127.0.0.1:8000/users/1
# {"id":1,"name":"Alice","email":"alice@test.com"}

curl http://127.0.0.1:8000/users/99
# {"detail":"User 99 not found"}

user_id from the path → get_user_from_dbget_db resolves first.

Exercise 2: A three-level chain (Medium)

Build a chain: get_settings()get_db(settings)get_active_users(db). get_settings returns a dict with max_users. get_db returns the user list. get_active_users filters and caps it according to max_users. Use it in GET /users.

See solution
from fastapi import FastAPI, Depends

app = FastAPI()

all_users = [
    {"id": 1, "name": "Alice", "active": True},
    {"id": 2, "name": "Bob", "active": False},
    {"id": 3, "name": "Charlie", "active": True},
    {"id": 4, "name": "Diana", "active": True},
    {"id": 5, "name": "Eve", "active": False},
]


def get_settings() -> dict:
    return {"max_users": 3, "app_name": "User API"}


def get_db(settings: dict = Depends(get_settings)) -> dict:
    return {"users": all_users, "max": settings["max_users"]}


def get_active_users(db: dict = Depends(get_db)) -> list:
    active = [u for u in db["users"] if u["active"]]
    return active[: db["max"]]


@app.get("/users")
def list_users(users: list = Depends(get_active_users)):
    return {"users": users, "count": len(users)}
curl http://127.0.0.1:8000/users
# {"users":[{"id":1,"name":"Alice",...},{"id":3,"name":"Charlie",...},{"id":4,"name":"Diana",...}],"count":3}

The chain: get_settingsget_dbget_active_userslist_users.

Exercise 3: A configurable class-based dependency (Medium)

Create an ItemFilter class that takes a field (field_name: str) in __init__ and a value query parameter (Optional[str]) in __call__. Create two instances: filter_by_category and filter_by_status. Use them in an endpoint to filter items.

See solution
from typing import Optional
from fastapi import FastAPI, Depends, Query

app = FastAPI()

items = [
    {"id": 1, "name": "Laptop", "category": "tech", "status": "available"},
    {"id": 2, "name": "Book", "category": "education", "status": "available"},
    {"id": 3, "name": "Monitor", "category": "tech", "status": "sold"},
    {"id": 4, "name": "Course", "category": "education", "status": "available"},
    {"id": 5, "name": "Keyboard", "category": "tech", "status": "sold"},
]


class ItemFilter:
    def __init__(self, field_name: str):
        self.field_name = field_name

    def __call__(self, value: Optional[str] = Query(default=None)) -> Optional[str]:
        return value


filter_by_category = ItemFilter("category")
filter_by_status = ItemFilter("status")


@app.get("/items")
def list_items(
    category: Optional[str] = Depends(filter_by_category),
    status: Optional[str] = Depends(filter_by_status),
):
    result = items[:]
    if category:
        result = [i for i in result if i["category"] == category]
    if status:
        result = [i for i in result if i["status"] == status]
    return {"total": len(result), "items": result}
curl "http://127.0.0.1:8000/items?value=tech"
# Filters by category=tech (the first query param mapped)

curl "http://127.0.0.1:8000/items"
# No filters, returns everything

Note: both instances share the value query param. In real practice, you'd use different names in __call__, or the filter architecture from the previous capsule.

Exercise 4: Composing auth + ownership (Medium-Hard)

Build a system where: get_current_user (reads the Authorization header), get_task_or_404 (looks up by ID), and verify_task_owner (a sub-dependency that uses both and checks ownership). Use verify_task_owner on PUT and DELETE but not on GET (any authenticated user can view).

See solution
from fastapi import FastAPI, Depends, Header, HTTPException
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI()

users = {
    "token-alice": {"id": 1, "name": "Alice"},
    "token-bob": {"id": 2, "name": "Bob"},
}

tasks = [
    {"id": 1, "title": "Alice's task", "owner_id": 1, "completed": False},
    {"id": 2, "title": "Bob's task", "owner_id": 2, "completed": False},
]


class TaskUpdate(BaseModel):
    title: Optional[str] = Field(default=None, min_length=1)
    completed: Optional[bool] = None


def get_current_user(authorization: str = Header(...)) -> dict:
    user = users.get(authorization)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user


def get_task_or_404(task_id: int) -> dict:
    for task in tasks:
        if task["id"] == task_id:
            return task
    raise HTTPException(status_code=404, detail=f"Task {task_id} not found")


def verify_task_owner(
    user: dict = Depends(get_current_user),
    task: dict = Depends(get_task_or_404),
) -> dict:
    if task["owner_id"] != user["id"]:
        raise HTTPException(status_code=403, detail="You don't own this task")
    return task


@app.get("/tasks/{task_id}")
def read_task(
    _user: dict = Depends(get_current_user),
    task: dict = Depends(get_task_or_404),
):
    return task


@app.put("/tasks/{task_id}")
def update_task(
    task: dict = Depends(verify_task_owner),
    data: TaskUpdate = ...,
):
    update = data.model_dump(exclude_unset=True)
    task.update(update)
    return task


@app.delete("/tasks/{task_id}")
def delete_task(task: dict = Depends(verify_task_owner)):
    tasks.remove(task)
    return {"message": "Deleted", "task": task}
# Alice reads her own task ✅
curl http://127.0.0.1:8000/tasks/1 -H "Authorization: token-alice"

# Alice reads Bob's task ✅ (GET doesn't check ownership)
curl http://127.0.0.1:8000/tasks/2 -H "Authorization: token-alice"

# Alice updates Bob's task ❌
curl -X PUT http://127.0.0.1:8000/tasks/2 \
  -H "Authorization: token-alice" \
  -H "Content-Type: application/json" \
  -d '{"title": "Hacked"}'
# {"detail":"You don't own this task"}

# Bob updates his own task ✅
curl -X PUT http://127.0.0.1:8000/tasks/2 \
  -H "Authorization: token-bob" \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated by Bob"}'

Exercise 5: A class-based RoleChecker (Hard)

Create a RoleChecker class that takes allowed_roles: list[str] in __init__. In __call__, use Depends(get_current_user) and check that the user's role is in the list. Create instances: allow_admin, allow_editor_or_admin. Apply them to different endpoints.

See solution
from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()

users = {
    "token-admin": {"id": 1, "name": "Admin", "role": "admin"},
    "token-editor": {"id": 2, "name": "Editor", "role": "editor"},
    "token-viewer": {"id": 3, "name": "Viewer", "role": "viewer"},
}


def get_current_user(authorization: str = Header(...)) -> dict:
    user = users.get(authorization)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user


class RoleChecker:
    def __init__(self, allowed_roles: list):
        self.allowed_roles = allowed_roles

    def __call__(self, user: dict = Depends(get_current_user)) -> dict:
        if user["role"] not in self.allowed_roles:
            raise HTTPException(
                status_code=403,
                detail=f"Role '{user['role']}' not in {self.allowed_roles}",
            )
        return user


allow_admin = RoleChecker(["admin"])
allow_editor_or_admin = RoleChecker(["admin", "editor"])
allow_any_authenticated = RoleChecker(["admin", "editor", "viewer"])


@app.get("/articles", dependencies=[Depends(allow_any_authenticated)])
def list_articles():
    return [{"id": 1, "title": "Article 1"}]


@app.post("/articles")
def create_article(user: dict = Depends(allow_editor_or_admin)):
    return {"created_by": user["name"]}


@app.delete("/articles/1")
def delete_article(user: dict = Depends(allow_admin)):
    return {"deleted_by": user["name"]}
# Viewer can read ✅
curl http://127.0.0.1:8000/articles -H "Authorization: token-viewer"

# Viewer cannot create ❌
curl -X POST http://127.0.0.1:8000/articles -H "Authorization: token-viewer"
# {"detail":"Role 'viewer' not in ['admin', 'editor']"}

# Editor can create ✅
curl -X POST http://127.0.0.1:8000/articles -H "Authorization: token-editor"
# {"created_by":"Editor"}

# Editor cannot delete ❌
curl -X DELETE http://127.0.0.1:8000/articles/1 -H "Authorization: token-editor"
# {"detail":"Role 'editor' not in ['admin']"}

# Admin can do everything ✅
curl -X DELETE http://127.0.0.1:8000/articles/1 -H "Authorization: token-admin"
# {"deleted_by":"Admin"}

Troubleshooting

Problem 1: "TypeError: get_current_user() missing required argument"

Cause: You called the function directly instead of using Depends().

# ❌ You're calling the function — that isn't DI
@app.get("/tasks")
def list_tasks(user=get_current_user()):
    ...

# ✅ You use Depends — FastAPI resolves the parameters
@app.get("/tasks")
def list_tasks(user: dict = Depends(get_current_user)):
    ...

Problem 2: The dependency chain causes a circular error

Cause: Dependency A depends on B, and B depends on A.

# ❌ Circular — FastAPI can't resolve it
def dep_a(b=Depends(dep_b)): ...
def dep_b(a=Depends(dep_a)): ...

Fix: refactor so that one dependency doesn't depend on the other. Extract the shared logic into a third dependency.

Problem 3: The class-based dependency receives unexpected parameters

Cause: You're confusing __init__ with __call__. The request's parameters (query, path, header) go in __call__; the configuration goes in __init__.

# ❌ max_limit in __call__ gets interpreted as a query parameter
class Paginator:
    def __call__(self, max_limit: int, skip: int = 0):
        ...

# ✅ max_limit in __init__, request parameters in __call__
class Paginator:
    def __init__(self, max_limit: int):
        self.max_limit = max_limit

    def __call__(self, skip: int = Query(default=0, ge=0)):
        ...

Problem 4: The sub-dependency doesn't run

Cause: You passed the result instance instead of Depends().

# ❌ Passes a literal dict, not a dependency
def get_data(settings={"debug": True}):
    ...

# ✅ Use Depends()
def get_data(settings: dict = Depends(get_settings)):
    ...

Problem 5: Two dependencies with the same query parameter cause a conflict

Cause: If two dependencies expect a query param with the same name, FastAPI uses the same value for both.

# ❌ Both read "limit" from the query string — they share the value
def dep_a(limit: int = Query(default=10)): ...
def dep_b(limit: int = Query(default=50)): ...

# ✅ Use different names, or encapsulate them in a single dependency
def dep_a(items_limit: int = Query(default=10, alias="items_limit")): ...
def dep_b(users_limit: int = Query(default=50, alias="users_limit")): ...

Summary

  • Sub-dependencies: a dependency function that uses Depends() in its own signature
  • FastAPI resolves dependency chains automatically, bottom-up
  • Class-based dependencies: classes with __call__ for configurable dependencies
  • __init__ takes the configuration, __call__ takes the request's parameters
  • Composition: combine small dependencies to build complex abstractions
  • Dependencies are cached per request — a shared sub-dependency runs only once
  • App-level dependencies: FastAPI(dependencies=[Depends(fn)]) applies to every endpoint
  • Common patterns: auth → permissions → ownership, settings → db → query

Additional resources

  1. FastAPI - Sub-dependencies — The official sub-dependencies documentation
  2. FastAPI - Classes as Dependencies — Class-based dependencies with call
  3. FastAPI - Global Dependencies — App-level dependencies
  4. FastAPI - Dependencies in decorators — The dependencies=[] syntax
  5. Python - call method — How call works in Python

What's next?

Next capsule: Yield Dependencies and Lifecycle — What happens when your dependency needs to do cleanup? With yield, a dependency can set up a resource, hand it to the endpoint, and clean it up afterward — perfect for database sessions, open files, and transactions. You'll also see dependency_overrides for replacing dependencies in testing.