Python typing.Annotated: Attach Metadata to Types
Python's typing.Annotated provides a standardized
mechanism for attaching arbitrary, non-type metadata to variable
declarations and type hints without altering how static type checkers
interpret them. Introduced in Python 3.9 via PEP 593, it bridges the gap
between static analysis and runtime tooling. This article explains the
role of typing.Annotated, how it functions with static type
checkers, how runtime frameworks leverage its metadata, and how to
inspect these annotations programmatically.
The Purpose of
typing.Annotated
Before typing.Annotated, developers and library authors
often struggled to attach runtime metadata to type hints. Common
workarounds included subclassing types, using custom wrapper classes, or
defining non-standard docstrings. These approaches frequently broke
static type checking with tools like Mypy and Pyright or caused runtime
overhead.
typing.Annotated solves this by separating the core type
from its contextual metadata. Its signature accepts a base type as the
first argument, followed by one or more arbitrary metadata objects:
from typing import Annotated
# Syntax: Annotated[BaseType, Metadata1, Metadata2, ...]
Age = Annotated[int, "Value must be between 0 and 120"]Static Type Checker Behavior
To static analysis tools (Mypy, Pyright, IDEs),
Annotated[T, ...] is treated identically to T.
The metadata elements are completely invisible to static type inference
and verification rules.
For example:
from typing import Annotated
PositiveInt = Annotated[int, "greater than zero"]
count: PositiveInt = 10
count = "ten" # Static type checker raises: Incompatible types (expression has type "str", variable has type "int")The type checker enforces that count must be an integer,
ignoring the string "greater than zero". This guarantees
that attaching operational metadata never degrades or complicates static
typing guarantees.
Accessing Metadata at Runtime
The metadata stored in Annotated is preserved at runtime
in the type's __metadata__ attribute. Standard inspection
tools like typing.get_type_hints() can extract this
information when configured with the include_extras=True
flag:
from typing import Annotated, get_type_hints
class UserProfile:
username: Annotated[str, {"max_length": 30, "unique": True}]
score: Annotated[int, "Must be positive"]
hints = get_type_hints(UserProfile, include_extras=True)
print(hints["username"].__metadata__)
# Output: ({'max_length': 30, 'unique': True},)
print(hints["score"].__metadata__)
# Output: ('Must be positive',)By default, calling get_type_hints(UserProfile) without
include_extras=True strips the Annotated
wrapper and returns the raw base types (str and
int), ensuring backward compatibility with tools that do
not expect metadata wrappers.
Primary Use Cases
Modern Python frameworks rely heavily on Annotated for
declarative design patterns:
- Validation and Constraints: Libraries like Pydantic
use
Annotatedalongside constraint specifiers (such asField()) to define boundaries, regex patterns, or serialization aliases directly within data models. - Dependency Injection: Web frameworks like FastAPI
use
Annotatedto inject dependencies (such as database sessions, headers, or query parameters) into route functions viaAnnotated[Session, Depends(get_db)]. - Database ORM Mapping: Modern ORMs use metadata to declare database column attributes, foreign keys, or indexing parameters alongside standard Python types.
By providing a clean, standard interface for attaching supplementary
information to types, typing.Annotated allows frameworks to
build rich runtime behaviors while maintaining strict, fully compatible
static type checking.