Sub-dependencies

Difficulty: Intermediate

Sub-dependencies are dependencies that themselves have dependencies. This creates a chain or tree of dependencies that FastAPI resolves automatically. Sub-dependencies are essential for building layered architectures where each layer builds on the previous one.

A common pattern is authentication layering: a low-level dependency validates the token, a mid-level dependency extracts the user, and a high-level dependency checks permissions. Each layer depends on the one below it. The handler only needs to depend on the highest-level dependency, and FastAPI resolves the entire chain.

FastAPI caches dependency results within a single request by default. If dependency A is used by both dependency B and dependency C, and both B and C are used in the same handler, dependency A is called only once. The cached result is reused. This prevents issues like creating multiple database connections for a single request.

You can disable caching by using Depends(dependency, use_cache=False). This forces FastAPI to call the dependency again even if it was already called in the same request. This is rarely needed but can be useful for dependencies that need fresh data on each call.

The dependency resolution order follows the tree from bottom to top. Leaf dependencies (those with no sub-dependencies) are resolved first, then their parents, and so on up to the root. Within the same level, dependencies are resolved in the order they appear in the function signature.

Sub-dependencies enable powerful patterns like middleware-style processing without actual middleware. For example, a logging dependency can wrap request handling, a timing dependency can measure execution time, and a transaction dependency can manage database transactions - all composable through the dependency chain.

Code examples

Authentication Chain

from fastapi import FastAPI, Depends, HTTPException, Header

app = FastAPI()

# Level 1: Extract token
def get_token(authorization: str = Header()):
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Invalid auth header")
    return authorization.split(" ")[1]

# Level 2: Validate token and get user (depends on Level 1)
def get_current_user(token: str = Depends(get_token)):
    users = {"valid-token": {"id": 1, "name": "Alice", "role": "admin"}}
    user = users.get(token)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

# Level 3: Check admin role (depends on Level 2)
def require_admin(user: dict = Depends(get_current_user)):
    if user.get("role") != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    return user

# Handler only depends on the highest level needed
@app.get("/profile")
def read_profile(user: dict = Depends(get_current_user)):
    return user

@app.get("/admin/settings")
def admin_settings(admin: dict = Depends(require_admin)):
    return {"settings": {}, "admin": admin["name"]}

Three dependency layers: get_token extracts the token, get_current_user validates it and looks up the user, require_admin checks the role. /profile needs user-level access, /admin/settings needs admin-level. FastAPI resolves the full chain automatically.

Dependency Caching

from fastapi import FastAPI, Depends

app = FastAPI()

call_count = 0

def get_db_session():
    global call_count
    call_count += 1
    print(f"Creating DB session (call #{call_count})")
    return {"session_id": call_count}

def get_user(db: dict = Depends(get_db_session)):
    return {"user": "Alice", "db_session": db["session_id"]}

def get_settings(db: dict = Depends(get_db_session)):
    return {"theme": "dark", "db_session": db["session_id"]}

@app.get("/dashboard")
def dashboard(
    user: dict = Depends(get_user),
    settings: dict = Depends(get_settings),
):
    # get_db_session is called ONCE, not twice!
    # Both get_user and get_settings receive the same session
    return {"user": user, "settings": settings}

# Output: "Creating DB session (call #1)" (printed once)
# Both user and settings use session_id: 1

Even though both get_user and get_settings depend on get_db_session, it is called only once per request. The cached result is shared. This is crucial for database sessions - you want one session per request, not one per dependency.

Composable Dependencies

from fastapi import FastAPI, Depends, Query

app = FastAPI()

# Base dependencies
def get_pagination(
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=20, ge=1, le=100),
):
    return {"page": page, "per_page": per_page, "skip": (page - 1) * per_page}

def get_sorting(
    sort_by: str = Query(default="created_at"),
    order: str = Query(default="desc"),
):
    return {"sort_by": sort_by, "order": order}

def get_search(
    q: str | None = Query(default=None),
):
    return {"query": q}

# Composed dependency
def get_list_params(
    pagination: dict = Depends(get_pagination),
    sorting: dict = Depends(get_sorting),
    search: dict = Depends(get_search),
):
    return {pagination, sorting, search}

@app.get("/products")
def list_products(params: dict = Depends(get_list_params)):
    return {"params": params, "products": []}

Small, focused dependencies (pagination, sorting, search) are composed into a single get_list_params dependency. This keeps each piece simple and testable while providing a clean interface to the handler.

Key points

Concepts covered

Dependency Chain, Nested Depends, Caching, use_cache, Dependency Tree