Class Dependencies

intermediate fastapi dependency-injection

A dependency doesn’t have to be a function. Anything callable works — including a class. When we pass a class to Depends(), FastAPI calls ClassName(...) for each request and injects the resulting instance.

In simple language — if our “dependency” is just a few parameters bundled together, or if it needs configuration we set at app startup, a class fits better than a function.

Why classes over functions

Functions are great for stateless dependencies (parse pagination, decode a token). But sometimes:

  • We want grouped parameters with attribute access (page.skip reads nicer than page["skip"]).
  • The dependency needs configuration injected at app startup (DB pool, Redis client, config object).
  • We want a clear, mockable interface for tests.

Classes give us all three.

Grouped params — the simplest case

Functions return a dict. Classes return an instance with typed attributes:

from fastapi import Depends, FastAPI

app = FastAPI()

class Pagination:
    def __init__(self, skip: int = 0, limit: int = 10):
        self.skip = skip
        self.limit = min(limit, 100)

@app.get("/users")
def list_users(page: Pagination = Depends(Pagination)):
    return {"skip": page.skip, "limit": page.limit}

FastAPI inspects __init__ exactly like it would inspect a function signature — skip and limit become query params, defaults flow through, Swagger picks them up.

The shortcut

When the dependency IS the class itself, repeating it feels silly:

def list_users(page: Pagination = Depends(Pagination)):  # noisy

FastAPI lets us write it as:

def list_users(page: Pagination = Depends()):  # cleaner

Same behavior, half the typing.

When to reach for a class
Function
stateless, returns a value, no config needed
Class
grouped params, needs constructor config, needs to be mocked in tests

Callable instances — the shared-state pattern

Here’s the killer pattern. We instantiate the class once at startup with config (DB URL, redis client, feature flag), then the instance itself becomes the dependency. Per request, FastAPI calls the instance (via __call__), not the class.

import redis
from fastapi import Depends, HTTPException, Header

class RateLimiter:
    def __init__(self, redis_client, max_per_minute: int):
        self.r = redis_client
        self.max = max_per_minute

    def __call__(self, x_api_key: str = Header(...)) -> str:
        key = f"ratelimit:{x_api_key}"
        count = self.r.incr(key)
        if count == 1:
            self.r.expire(key, 60)
        if count > self.max:
            raise HTTPException(429, "too many requests")
        return x_api_key

# Build once at startup
rate_limit = RateLimiter(redis.Redis(), max_per_minute=60)

@app.get("/search")
def search(api_key: str = Depends(rate_limit)):
    return {"caller": api_key}

Why this rules:

  • The Redis client is created once, not per request.
  • The max_per_minute is config we control at startup.
  • The class is trivial to swap in tests — instantiate with a fake redis and a different limit.

Testability — the real argument

Compare. With a function:

def get_db() -> Session:
    return Session(global_engine)  # hardcoded global

Testing this means monkey-patching the global. Ugly.

With a class:

class DBProvider:
    def __init__(self, engine):
        self.engine = engine
    def __call__(self) -> Session:
        return Session(self.engine)

db_provider = DBProvider(engine=prod_engine)

# In tests
app.dependency_overrides[db_provider] = DBProvider(engine=test_engine)

We swap the entire provider at the DI boundary. Zero monkey-patching, zero globals.

Sub-deps work the same

A class can declare other Depends() in __init__ (or __call__ for callable instances):

class AuditedDB:
    def __init__(
        self,
        db: Session = Depends(get_db),
        user: User = Depends(get_current_user),
    ):
        self.db = db
        self.user = user

    def log_and_query(self, sql: str):
        audit(self.user.id, sql)
        return self.db.execute(sql)

The takeaway — function dependencies for simple stateless work, class dependencies (especially callable instances) when we need startup config, grouped state, or clean test seams. The Depends() API is the same; only the callable changes.