Skip to content

The power of Annotated[] is being unleashed

Annotated[] type arrived at Python 3.6 as per PEP 527. It is now getting more use cases.

What is it anyway?

From mypy point of view, Annotated[T, foo, bar, baz, ...] is strictly equivalent to T, its first argument. The remaining arguments can be any Python objects. For instance,

greeting: Annotated[str, 5, ..., [], 'boombadam', float]

is valid.

Why to use it?

To attach additional metadata to type annotations in Python. Even though mypy does not care for said metadata, other libraries might make use of it.

Use Cases

This table is a snapshot of things how they are at the moment of writing this post. Things are going to change.

Library declaring the annotation Annotation Library using the annotation Purpose
annotated-types Gt Lt Ge Le Interval pydantic > < ⩾ ⩽ and generic interval validators
MinLen MaxLen Len Sequence length validators
Timezone Validate timezone for a datetime value
Predicate Validate value satisfies a given predicate
GroupedMetadata Combine multiple annotations into one
pydantic Strict Validate the value in strict mode
Field Specify Pydantic validation rules
typing-extensions doc mkdocstrings Document a variable, function parameter, or class field
typing , starting at Python 3.13 (if PEP 727 passes)
FastAPI Query FastAPI Designate this as a query parameter
Path Designate as a path parameter
Body Designate as a body parameter
Depends Inject a dependency
Typer Argument Option Typer Designate function parameter as CLI option or argument.

Documenting function arguments

One of the cases above I would want to attract your attention to is doc() annotation, proposed by PEP 727 and best illustrated by an example:

from typing import Annotated, doc


def create_user(
    lastname: Annotated[str, doc("The **last name** of the newly created user")],
    firstname: Annotated[str | None, doc("The user's **first name**")] = None,
) -> Annotated[User, doc("The created user after saving in the database")]:
    """
    Create a new user in the system, it needs the database connection to be already
    initialized.
    """

This is another way to document function parameters, in addition to two existing methods:

  • in the docstring,
  • and via inline comments.

Let's compare those approaches.

Scope

I am intentionally leaving out other applications of doc() — annotation of variables and class fields. I believe that function signature annotations are much more often met in real code than the other use cases and thus will be most substantial to the overall developer experience.

Docstrings Inline comments doc()
Standardized? 🟡 Multiple competing standards
Google, numpy, and more
🔴 No
Or nothing I know of
🟢 Yes
by PEP 727
Looking at parameter definition, how easy it is to locate its description? 🟡 Moderately easy
One has to scroll to the docstring and find it there
🟢 Very easy 🟢 Very easy
How verbose is function signature? 🟢 Not verbose 🟡 A bit more verbose
Comments added
🔴 Significantly more verbose
Annotated[…, doc('…')] can be lengthy
How verbose is the docstring? 🔴 Verbose
Every parameter has to be detailed there
🟢 Not verbose 🟢 Not verbose
How hard is it to extract parameter documentation programmatically? 🔴 Very hard
Support multiple standards and recognize which is used
🟡 Moderately hard
Forces to extract docs from Python AST
🟢 Very easy
something.__annotations__ gets the job done

Verbosity

Verbosity of function signature is the most prevalently articulated argument against using Annotated[] widely. However, it might be argued that verbosity does not appear out of the blue; it is just being moved from the docstring to the signature — and thus made arguably more manageable.

More ideas for annotations

If you have a hammer ⇒ everything around looks like a nail.

Annotated[dict, MutatedArgument('Accumulator to store cached `Badabazinga` instances.')]
If the argument of a function is mutated in the function, convey that information for documentation and linters. By default, functions should not do that; but if they do — this should be very clearly evident.

Annotated[str, TODO('Use an `Enum` instead of a plain string.', ticket='PRJ-123')]
Make TODO and FIXME markers more semantic and integrate them into project docs. IDE might show this hint every time the user hovers over the annotated value, even at a completely different place in the code.

Do you have anything in mind? Feel free to :material-github: submit a PR.

Conclusion

I do not care for increased verbosity that the reliance on Annotated[] introduces to type annotations for parameters, fields, and variables, as long as

  • repetition is avoided,
  • and the annotations provide both proteine- and silicon-based developers with semantically rich information about the code they are reading.

Looking forward to making my code more readable and documentable then it ever was.