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 verboseAnnotated[…, 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 easysomething.__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.')]
Annotated[str, TODO('Use an `Enum` instead of a plain string.', ticket='PRJ-123')]
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.