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:
- with the expected
commons
parameters, then those given parameters are used - without the expected
commons
parameters, then the function insideDepends
gets called and its return value is assigned tocommons
as the functionread_items
runs.
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 ofDepends(CommonQueryParams)
.
And this is why this pattern works.
Conclusion
To summarize:
Annotated
by itself does not do anything, other than give a type hint plus any potential metadata one would find useful.- In the context of FastAPI:
Depends
can be used as a metadata argument forAnnotated
to inject dependencies. I think this works as long as the first function called is decorated by something from FastAPI.- If
Depends
is used inAnnotated
with no arguments, thenDepends
calls the class which was given as the first argument inAnnotated
.