Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Callable Classes for Parameterized Dependencies

Recommended resources

Hello! Welcome to our next lesson on FastAPI's Dependency Injection system.

In the previous lesson, we explored how to create and use simple function-based dependencies with Depends(). We saw how they help keep our code DRY (Don't Repeat Yourself) by abstracting common logic like pagination and how they can be chained to build complex validation flows, like fetching a current user.

However, function-based dependencies have a limitation. Their parameters are resolved by FastAPI from the request (path, query, headers, etc.). What if you need to pass a specific configuration to your dependency when you declare it? For instance, how would you create a single, reusable dependency to check for different user roles ("admin", "manager", "editor")?

This is where class-based dependencies shine. Today's lesson addresses the learning outcome: Build reusable, parameterized dependencies using callable classes. We will explore how using Python classes as dependencies unlocks more powerful and flexible patterns, moving us closer to the kind of sophisticated, reusable components you might be familiar with from Django REST Framework's permission classes.

1. From Functions to Classes: Grouping Parameters

Let's start by evolving a pattern we've already seen. In the last lesson, we created a function to handle common query parameters for pagination:

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):



    # use commons["q"], commons["skip"], etc.
    ...

While this works, it has a drawback: the commons parameter is just a dict. Your editor doesn't know what keys it contains, so you lose out on autocompletion and type checking. We can solve this by converting the dependency into a class.

Classes as Dependencies - FastAPI

The official FastAPI documentation provides a clear guide on this exact refactoring. It will show you how to convert a function dependency into a class to get better tooling support and code organization.

Please read the sections 'What makes a dependency', 'Classes as dependencies', and 'Shortcut'. Focus on how the common_parameters function is transformed into the CommonQueryParams class and how the path operation signature changes. Pay attention to the role of the __init__ method.

As you've just read, FastAPI treats any "callable" as a potential dependency. Since a class MyClass(...) is called to create an instance, it fits this definition perfectly.

Here's the refactored code:

from typing import Annotated
from fastapi import Depends, FastAPI

app = FastAPI()




# 1. The class-based dependency
class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit




# 2. The path operation using the class
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends()]):



    # Now you get full autocompletion for commons.q, commons.skip, etc.
    # FastAPI creates an instance of CommonQueryParams and injects it.
    ...

How it works:

  1. FastAPI sees that read_items depends on CommonQueryParams.
  2. It inspects the __init__ method of CommonQueryParams and sees it needs q, skip, and limit.
  3. It resolves these parameters from the incoming request's query string, just as it would for a function.
  4. It calls CommonQueryParams(q="...", skip=..., limit=...) to create an instance.
  5. This new instance is passed as the commons argument to your read_items function.

The main advantage here is improved developer experience—your code is now strongly typed and self-documenting.


2. Parameterized Dependencies: The Real Power of Classes

Now we get to the core of this lesson: creating dependencies that accept configuration. This is where we define a class with both an __init__ method for configuration and a __call__ method for the request-time logic.

This pattern is incredibly useful and is analogous to how you would build a custom, configurable permission class in DRF. Imagine you want a dependency that checks if a user has a specific role. You don't want to write a separate dependency for each role (IsAdmin, IsEditor, etc.). You want one RoleChecker that you can configure like this:

Depends(RoleChecker(required_role="admin"))

Let's see how to build this.

Dependencies with Arguments - FastapiTutorial

This article from FastAPITutorial.com provides a great example of a dependency that needs custom arguments. It demonstrates why a simple function isn't enough and how to solve the problem using a class with __init__ and __call__.

Read the entire article. It's short. First, understand the problem it presents with the get_object_or_featured function. Then, carefully analyze the solution using the GetObjectOrFeatured class. Note the distinct roles of __init__ and __call__.

The key insight from the article is the separation of concerns:

  • The __init__ method is called once, when you define the dependency in your code. This is where you pass your configuration.
  • The __call__ method makes the instance of the class a callable. FastAPI executes this method for every request that uses the dependency. This is where you put your request-time logic.

Building a Reusable Role Checker

Let's apply this pattern to our role-based access control (RBAC) idea. This should feel familiar to your experience with Django's permission system.

First, let's set up some placeholders for a user model and a function to get the current user. In a real application, get_current_user would decode a JWT and fetch the user from a database, as we glimpsed in the previous lesson.

from fastapi import Depends, FastAPI, HTTPException, status
from pydantic import BaseModel




# A placeholder user model
class User(BaseModel):
    username: str
    role: str




# A placeholder dependency to simulate getting the current user
# In a real app, this would come from an OAuth2PasswordBearer dependency
async def get_current_user() -> User:



    # This would normally be decoded from a token
    return User(username="testuser", role="editor")

Now, let's build our parameterized RoleChecker dependency.

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



        # 1. The configuration is stored in the instance
        self.required_role = required_role

    def __call__(self, user: User = Depends(get_current_user)) -> User:



        # 2. This logic runs for each request
        if user.role != self.required_role:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"User does not have the required '{self.required_role}' role.",
            )
        return user

Let's break this down:

  1. __init__(self, required_role: str): When we create an instance, like RoleChecker("admin"), this method stores "admin" in self.required_role.
  2. __call__(self, user: User = Depends(get_current_user)): This is the logic FastAPI runs.
    • Notice it has its own dependency: it needs the current_user to do its job.
    • It compares the user's role to the self.required_role that was configured during initialization.
    • If the check fails, it raises an HTTPException. If it passes, it returns the user object, which can then be used by the path operation.

Using the Parameterized Dependency

Now we can use this single, reusable class to protect different endpoints with different role requirements.

app = FastAPI()




# An instance of our dependency configured for 'admin' role
admin_role_checker = RoleChecker(required_role="admin")




# An instance configured for 'editor' role
editor_role_checker = RoleChecker(required_role="editor")

@app.get("/admin/dashboard")
async def get_admin_dashboard(user: User = Depends(admin_role_checker)):
    return {"message": f"Welcome to the admin dashboard, {user.username}!"}

@app.get("/editor/content")
async def get_editor_content(user: User = Depends(editor_role_checker)):
    return {"message": f"Welcome to the content editor, {user.username}!"}

If you were to run this code and make a request to /admin/dashboard, our simulated get_current_user would return a user with the role "editor". The admin_role_checker would see that "editor" does not match its required role of "admin" and immediately return a 403 Forbidden error. The request to /editor/content, however, would succeed.

This pattern is extremely powerful and clean. You have created a single, testable, and reusable component for a critical cross-cutting concern, just as you would with a custom BasePermission class in DRF.


Conclusion

In this lesson, we've elevated our understanding of FastAPI's dependency injection system by incorporating classes.

Key Takeaways:

  • Dependencies can be any Python "callable", including classes.
  • Simple Class Dependencies: Using a class with just an __init__ method is a great way to group related parameters (like for pagination), providing better type hinting and code organization than returning a dict.
  • Parameterized Class Dependencies: The __init__ / __call__ pattern is the key to creating configurable dependencies. __init__ handles the one-time configuration, and __call__ contains the request-time logic.
  • This pattern is ideal for building reusable components for concerns like authentication, authorization (RBAC), logging, or any other logic that needs to be both repeated and configurable.

Preview of the Next Lesson:

We've now seen function-based dependencies and two types of class-based dependencies. We've also seen how dependencies can depend on other dependencies. In our next lesson, "Chain nested dependencies to compose complex, reusable logic," we will dive deeper into how these different types of dependencies can be composed into sophisticated execution trees. We'll also prepare for a crucial upcoming topic: using yield within dependencies to manage resources that have setup and teardown phases, such as database connections or file handles.