Skip to content

Using Annotated

Why?

Using Annotated has several benefits, one of the main ones is that now the parameters of your functions with Annotated would not be affected at all.

If you call those functions in other places in your code, the actual default values will be kept, your editor will help you notice missing required arguments, Python will require you to pass required arguments at runtime, you will be able to use the same functions for different things and with different libraries.

Because Annotated is standard Python, you still get all the benefits from editors and tools, like autocompletion, inline errors, etc.

One of the biggest benefits is that now you can create Annotated dependencies that are then shared by multiple path operation functions, this will allow you to reduce a lot of code duplication in your codebase, while keeping all the support from editors and tools.

Example

For example, you could have code like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from fast_depends import Depends, inject
from pydantic import BaseModel, PositiveInt

class User(BaseModel):
    user_id: PositiveInt

def get_user(user: id) -> User:
    return User(user_id=user)

@inject
def do_smth_with_user(user: User = Depends(get_user)):
    ...

@inject
def do_another_smth_with_user(user: User = Depends(get_user)):
    ...

There's a bit of code duplication for the dependency:

user: User = Depends(get_user)

...the bigger the codebase, the more noticeable it is.

Now you can create an annotated dependency once, like this:

CurrentUser = Annotated[User, Depends(get_user)]

And then you can reuse this Annotated dependency:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from typing import Annotated
from fast_depends import Depends, inject
from pydantic import BaseModel, PositiveInt

class User(BaseModel):
    user_id: PositiveInt

def get_user(user: id) -> User:
    return User(user_id=user)

CurrentUser = Annotated[User, Depends(get_user)]

@inject
def do_smth_with_user(user: CurrentUser):
    ...

@inject
def do_another_smth_with_user(user: CurrentUser):
    ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from typing_extensions import Annotated
from fast_depends import Depends, inject
from pydantic import BaseModel, PositiveInt

class User(BaseModel):
    user_id: PositiveInt

def get_user(user: id) -> User:
    return User(user_id=user)

CurrentUser = Annotated[User, Depends(get_user)]

@inject
def do_smth_with_user(user: CurrentUser):
    ...

@inject
def do_another_smth_with_user(user: CurrentUser):
    ...

...and CurrentUser has all the typing information as User, so your editor will work as expected (autocompletion and everything), and FastDepends will be able to understand the dependency defined in Annotated. 😎

Annotatable variants

You able to use Field and Depends with Annotated as well

1
2
3
4
5
6
from typing import Annotated
from fast_depends import Depends
from pydantic import Field

CurrentUser = Annotated[User, Depends(get_user)]
MaxLenField = Annotated[str, Field(..., max_length="32")]
1
2
3
4
5
6
from typing_extensions import Annotated
from fast_depends import Depends
from pydantic import Field

CurrentUser = Annotated[User, Depends(get_user)]
MaxLenField = Annotated[str, Field(..., max_length="32")]

Limitations

Python has a very structured function arguments declaration rules.

def function(
    required_positional_or_keyword_arguments_first,
    default_positional_or_keyword_arguments_second = None.
    *all_unrecognized_positional_arguments,
    required_keyword_only_arguments,
    default_keyword_only_arguments = None,
    **all_unrecognized_keyword_arguments,
): ...

Warning

You can not declare arguments without default after default arguments was declared

So

def func(user_id: int, user: CurrentUser): ...

... is a valid python code

But

def func(user_id: int | None = None, user: CurrentUser): ...  # invalid code!

... is not! You can't use the Annotated only argument after default argument declaration.


There are some ways to write code above correct way:

You can use Annotated with a default value

def func(user_id: int | None = None, user: CurrentUser = None): ...

Or you you can use Annotated with all arguments

UserId = Annotated[int, Field(...)]  # Field(...) is a required
def func(user_id: UserId, user: CurrentUser): ...

Also, from the Python view, the following code

# Potential invalid code!
def func(user: CurrentUser, user_id: int | None = None): ...

But, FastDepends parse positional arguments according their position.

So, calling the function above this way

func(1)

Will parses as the following kwargs

{ "user": 1 }
And raises error

But, named calling will be correct

func(user_id=1)  # correct calling

I really recommend do not use Annotated as a positional argument

The best way to avoid all missunderstanding between you and Python - use pydantic.Field with Annotated everywhere

Like in the following example

def func(user_id: Annotated[int, Field(...)], user: CurrentUser): ...