How to Use the Cool New Annotated Typing Feature of FastAPI

Image by Nubelson Fernandes on Unsplash

In the latest release of FastAPI (v0.95 at the time of writing), there is a cool new feature for adding type annotations. It is Annotated that allows you to add some metadata to your query/path parameters, headers, and even dependencies. There are many benefits of using Annotated for type annotations, including better editor support, easier-to-share annotated functions, and reduction of code duplication in your codebase. However, it also has some pitfalls in its current state that may cost you hours of debugging time.

Let’s see how it works in practice because it’s much easier to learn it by coding than by talking.


Preparation

If you want to follow along, you need to install the dependencies for FastAPI and uvicorn. The versions are important here because I’m sure the bugs shown in this post will be solved in a new release. Nevertheless, knowing the bugs can save you hours of painful debugging time if you happen to use this version of FastAPI (0.95.0) as me. Or if this bug takes a long time to solve.

It’s recommended to create a virtual environment for our project as we can install different versions of Python and libraries in it.

conda create -n fastapi-v95 python=3.11
conda activate fastapi-v95

pip install fastapi==0.95.0 uvicorn==0.21.1

Create a simple API

We will create a very simple FastAPI endpoint that has a query parameter for demonstration:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/")
async def hello(name: str | None = Query(default="World", alias="n")):
    return {"Hello": name}

Query is used for the demonstration of Annotated later.

We can spin up a server locally with uvicorn:

uvicorn main:app --port 8000 --reload

Now when you call the API, you can see the default query parameter returned if none is passed:

curl http://127.0.0.1:8000
# {"Hello":"World"}

curl http://127.0.0.1:8000?n=Lynn
# {"Hello":"Lynn"}

Up to now, everything is classical, and nothing new or surprising.


Use Annotated

Now let’s use the new Annotated feature introduced in FastAPI 0.95.

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/")
async def hello(name: Annotated[str | None, Query(alias="n")] = "World"):
    return {"Hello": name}

With Annotated, the first type parameter (here str | None) passed to Annotated is the actual type and the rest is just metadata for other tools (here FastAPI). You can have anything as the metadata, and it’s up to the other tools how to use it.

Importantly, using Annotated, the default value is specified after the equal sign (=), and should not be specified in Query anymore.

Since we use --reload with uvicorn, the API code should be reloaded automatically. And if you call the endpoint again, everything should work properly as demonstrated above.


Use Annotated with APIRouter

Now it’s the fun part. Let’s try to break the code with APIRouter:

from typing import Annotated

from fastapi import APIRouter, FastAPI, Query

app = FastAPI()

router = APIRouter()

@router.get("/")
async def hello(name: Annotated[str | None, Query(alias="n")] = "World"):
    return {"Hello": name}

app.include_router(router)

This time the server crashed with a weird error saying that Query default value cannot be set in Annotated and needs to be set by the equal sign “=” instead:

assert field_info.default is Undefined or field_info.default is Required, (
AssertionError: `Query` default value cannot be set in `Annotated` for 'name'.
Set the default value with `=` instead.

This error is not helpful at all because we are using Annotated as instructed in the official document, and we didn’t set a default value for Query inside Annotated. It’s specified with the equal sign. So why does it crash?

I tried to Google this problem with all kinds of keywords but was unable to find an answer (at the time of writing). And I tried to debug all the API code as well as the source code but still could not find a clue.

I even tried to rule out problems in library dependencies in my project’s requirements.txt, but couldn’t find anything suspicious there either.

Finally, when I was almost ready to give up, I said to myself, there must be something different in my code that breaks the Annotated feature.

So I started to compare the official code with my project code line by line. And after hours of debugging (which has the side effect of letting me get more familiar with my project code by the way 😏), I finally found that the APIRouter is something not covered in the official documentation for Annotated.

Then I tried to remove the APIRouter and it worked, as shown in the second example above.

However, removing the APIRouter instances will mean a major refactoring of the project code and it’s a very bad idea.

Instead, we can use the old style without Annotated when default values are required and thus end in a hybrid state. Yes, this is not perfect and may look ugly to many perfectionists, including me. However, it’s a workaround if you want to use the latest Annotated feature in your API code together with APIRouter, before the FastAPI developers fix this bug.

This is the “hybrid” code:

from typing import Annotated

from fastapi import APIRouter, FastAPI, Path, Query

app = FastAPI()

router = APIRouter()


@router.get("/{greet}")
async def hello(
    greet: Annotated[str, Path()],
    name: str | None = Query(default="World", alias="n"),
):
    return {greet: name}


app.include_router(router)

In order to create a “hybrid” state, a new Path parameter is introduced above. And now the server can be started properly and everything is working as expected:

curl http://127.0.0.1:8000/hello
# {"hello":"World"}

curl http://127.0.0.1:8000/hey
# {"hey":"World"}

curl http://127.0.0.1:8000/hi?n=Lynn
# {"hi":"Lynn"}

Cheers! Problem “solved”!


In this post, we have introduced the new Annotated typing feature of FastAPI. Annotated is a very cool feature and has many advantages. However, it also has some bugs in its current state. Using some simple-to-follow examples, we have covered how to use it properly and how to avoid the pitfalls with APIRouter. These small tips can potentially save you hours of debugging time.


Related articles:



Leave a comment

Blog at WordPress.com.