GUISSMO

Understanding FastAPI Annotated and Depends Pattern

Today, I investigated step-by-step how and why the code Annotated[ClassName, Depends()] works and why it could be useful if you want type hints.

I’m writing this down as I understand it and if anyone reading finds errors, I would appreciate it if you contact me!

type hints and Annotated

In my first backend project, when I asked a colleague what the above pattern does, I was told it had something to do with dependency injection and was kindly pointed to the relevant FastAPI page.

I started with Annotated in the typing documentation.

typing.Annotated

Special typing form to add context-specific metadata to an annotation.

After some experimentation, we find that:

def foo(x: int) -> bool:
    return x == 1

is functionally the same as:

from typing import Annotated
def foo(x: Annotated[int, "This is an integer between 0 and 2."]) -> bool:
    return x == 1

As long as the first argument on Annotated is a type, you’re good. You can write whatever you want on the rest of the arguments.

A priori, you get exactly the same benefits if you simply add a comment like so:

def foo(x: int) -> bool:
    """
    x is an integer between 0 and 2
    """
    return x == 1

At this point, I understood that the rest of the arguments are Annotated are ignored by Python so they could literally be anything. It turns out I was half right.

FastAPI, Annotated and Depends

Indeed, the rest of the arguments of Annotated could literally be anything but libraries such as FastAPI could utilize the metadata in Annotated in a more useful way.

To understand this, here is an example in their documentation on how to use Annotated and Depends.

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


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: Annotated[dict, Depends(common_parameters)]):
    return commons

The documentation did not adequately explain for me why this works. Upon further investigation, if the /items/ endpoint is called:

This only seems to work if the first function, in this case read_items, has been decorated by something from FastAPI, in this case @app.get("/items/").

Without such a decorator, read_items would complain that it was expecting an argument commons that was not given.

Class Dependencies

Once equipped with this understanding, the classes as dependencies page in the official FastAPI docs becomes clearer.

class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit

@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):

Like in the previous example, if the commons parameter is not given in the call of the endpoint then the argument of Depends, which in this case is CommonQueryParams is called with default arguments.

This returns an instance of CommonQueryParams and is thus a valid value for commons.

Why Annotated[ClassName, Depends()] Works

Clearly, we would find ourselves writing a useful pattern like Annotated[CommonQueryParams, Depends(CommonQueryParams)] many times in our code.

Notice that we would need to type the class name twice every time. The developers of FastAPI recognize this and have thus included the following feature as a shortcut:

You declare the dependency as the type of the parameter, and you use Depends() without any parameter, instead of having to write the full class again inside of Depends(CommonQueryParams).

And this is why this pattern works.

Conclusion

To summarize:

Back to Top | Blog RSS Feed