annotated-types
annotated-types provides reusable constraint metadata objects—such as Gt, Lt, Len, MultipleOf, Timezone, Predicate, and more—to be used with typing.Annotated (PEP 593). It does not enforce constraints itself; enforcement is left to consuming libraries like Pydantic, Hypothesis, or custom validators. Current version is 0.7.0 (released 2024). The project follows an irregular, feature-driven release cadence and was created at PyCon 2022 by the Pydantic and Hypothesis maintainers.
Warnings
- breaking Import module name uses underscores: `annotated_types`, not `annotated-types`. Using a hyphen causes a SyntaxError.
- breaking In v0.4.0, Len's kwargs were renamed and the semantics of the upper bound changed: `min_inclusive` → `min_length` (same meaning), `max_exclusive` → `max_length` (now INCLUSIVE). Code using old kwargs or assuming exclusive upper bound will silently produce wrong constraints.
- breaking `IsDigit` was renamed to `IsDigits` in v0.7.0. Importing `IsDigit` raises an ImportError on 0.7.0+.
- breaking Python 3.7 support was dropped in v0.6.0. The minimum required Python version is now 3.8.
- gotcha annotated-types does NOT enforce constraints at runtime itself. Metadata is purely declarative; enforcement depends entirely on the consuming library (e.g. Pydantic, Hypothesis). Annotating a field does not validate values without an active consumer.
- gotcha MultipleOf has two semantically different interpretations: Python modulo (`value % multiple_of == 0`) vs. JSONSchema (`int(value / multiple_of) == value / multiple_of`). For floats, these can silently diverge due to floating-point imprecision.
- gotcha Using lambda functions in Predicate prevents consuming libraries from introspecting the predicate for schema generation or targeted optimisations. Libraries may silently ignore or mishandle opaque lambdas.
Install
-
pip install annotated-types
Imports
- Gt, Lt, Ge, Le, Interval
from annotated_types import Gt, Lt, Ge, Le, Interval
- Len, MinLen, MaxLen
from annotated_types import Len, MinLen, MaxLen
- Predicate
from annotated_types import Predicate
- Timezone
from annotated_types import Timezone
- IsDigits
from annotated_types import IsDigits
- Unit
from annotated_types import Unit
- GroupedMetadata
from annotated_types import GroupedMetadata
Quickstart
from typing import Annotated, get_args, get_origin
import math
from annotated_types import Gt, Lt, Len, MinLen, MaxLen, MultipleOf, Predicate, Interval, Timezone, IsDigits
from datetime import datetime, timezone
# Scalar bounds
PositiveInt = Annotated[int, Gt(0)]
SmallFloat = Annotated[float, Interval(ge=0.0, le=1.0)]
# Collection length (both bounds inclusive since v0.4.0)
ShortList = Annotated[list, Len(1, 10)]
NonEmptyStr = Annotated[str, MinLen(1)]
CappedStr = Annotated[str, MaxLen(255)]
# Predicate — prefer introspectable callables over lambdas
FiniteFloat = Annotated[float, Predicate(math.isfinite)]
DigitOnly = Annotated[str, IsDigits]
# Timezone (v0.7.0+: accepts tzinfo objects)
UTCDatetime = Annotated[datetime, Timezone(timezone.utc)]
AwareDatetime = Annotated[datetime, Timezone(...)]
NaiveDatetime = Annotated[datetime, Timezone(None)]
# Reading metadata back (for library authors)
def show_constraints(tp):
if get_origin(tp) is Annotated:
base, *metadata = get_args(tp)
print(f"Base type: {base}, constraints: {metadata}")
show_constraints(Annotated[int, Gt(0), Lt(100)])