annotated-types
PEP-593 added typing.Annotated
as a way of adding context-specific metadata to existing types, and specifies that Annotated[T, x]
should be treated as T
by any tool or library without special logic for x
.
This package provides metadata objects which can be used to represent common constraints such as upper and lower bounds on scalar values and collection sizes, a Predicate
marker for runtime checks, and non-normative descriptions of how we intend these metadata to be interpreted. In some cases, we also note alternative representations which do not require this package.
Install
pip install annotated-types
Examples
from typing import Annotated
from annotated_types import Gt, Len
class MyClass:
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
# Invalid: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
# Invalid: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], 0:10] # Valid: [], [10, 20, 30, 40, 50]
# Invalid: (1, 2), ["abc"], [0] * 20
your_set: Annotated[set[int], Len(0, 10)] # Valid: {1, 2, 3}, ...
# Invalid: "Well, you get the idea!"
Documentation
While annotated-types
avoids runtime checks for performance, users should not construct invalid combinations such as MultipleOf("non-numeric")
or Annotated[int, Len(3)]
. Downstream implementors may choose to raise an error, emit a warning, silently ignore a metadata item, etc., if the metadata objects described below are used with an incompatible type - or for any other reason!
Gt, Ge, Lt, Le
Express inclusive and/or exclusive bounds on orderable values - which may be numbers, dates, times, strings, sets, etc. Note that the boundary value need not be of the same type that was annotated, so long as they can be compared: Annotated[int, Gt(1.5)]
is fine, for example, and implies that the value is an integer x such that x > 1.5
. No interpretation is specified for special values such as nan
.
We suggest that implementors may also interpret functools.partial(operator.le, 1.5)
as being equivalent to Gt(1.5)
, for users who wish to avoid a runtime dependency on the annotated-types
package.
To be explicit, these types have the following meanings:
Gt(x)
- value must be "Greater Than"x
- equivalent to exclusive minimumGe(x)
- value must be "Greater than or Equal" tox
- equivalent to inclusive minimumLt(x)
- value must be "Less Than"x
- equivalent to exclusive maximumLe(x)
- value must be "Less than or Equal" tox
- equivalent to inclusive maximum
Interval
Interval(gt, ge, lt, le)
allows you to specify an upper and lower bound with a single metadata object. None
attributes should be ignored, and non-None
attributes treated as per the single bounds above.
MultipleOf
MultipleOf(multiple_of=x)
might be interpreted in two ways:
- Python semantics, implying
value % multiple_of == 0
, or - JSONschema semantics, where
int(value / multiple_of) == value / multiple_of
.
We encourage users to be aware of these two common interpretations and their distinct behaviours, especially since very large or non-integer numbers make it easy to cause silent data corruption due to floating-point imprecision.
We encourage libraries to carefully document which interpretation they implement.
Len
Len()
implies that min_inclusive <= len(value) < max_exclusive
. We recommend that libraries interpret slice
objects identically to Len()
, making all the following cases equivalent:
Annotated[list, :10]
Annotated[list, 0:10]
Annotated[list, None:10]
Annotated[list, slice(0, 10)]
Annotated[list, Len(0, 10)]
Annotated[list, Len(max_exclusive=10)]
And of course you can describe lists of three or more elements (Len(min_inclusive=3)
), four, five, or six elements (Len(4, 7)
- note exclusive-maximum!) or exactly eight elements (Len(8, 9)
).
Implementors: note that Len() should always have an integer value for min_inclusive
, but slice
objects can also have start=None
.
Timezone
Timezone
can be used with a datetime
or a time
to express which timezones are allowed. Annotated[datetime, Timezone[None]]
must be a naive datetime. Timezone[...]
(literal ellipsis) expresses that any timezone-aware datetime is allowed. You may also pass a specific timezone string or timezone
object such as Timezone[timezone.utc]
or Timezone["Africa/Abidjan"]
to express that you only allow a specific timezone, though we note that this is often a symptom of fragile design.
Predicate
Predicate(func: Callable)
expresses that func(value)
is truthy for valid values. Users should prefer the statically inspectable metadata above, but if you need the full power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints: IsLower = Predicate(str.islower)
, IsUpper = Predicate(str.isupper)
, and IsDigit = Predicate(str.isdigit)
. Users are encouraged to use methods which can be given special handling, and avoid indirection like lambda s: s.lower()
.
Some libraries might have special logic to handle known or understandable predicates, for example by checking for str.isdigit
and using its presence to both call custom logic to enforce digit-only strings, and customise some generated external schema.
We do not specify what behaviour should be expected for predicates that raise an exception. For example Annotated[int, Predicate(str.isdigit)]
might silently skip invalid constraints, or statically raise an error; or it might try calling it and then propogate or discard the resulting TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object
exception. We encourage libraries to document the behaviour they choose.
Design & History
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic and Hypothesis, with the goal of making it as easy as possible for end-users to provide more informative annotations for use by runtime libraries.
It is deliberately minimal, and following PEP-593 allows considerable downstream discretion in what (if anything!) they choose to support. Nonetheless, we expect that staying simple and covering only the most common use-cases will give users and maintainers the best experience we can. If you'd like more constraints for your types - follow our lead, by defining them and documenting them downstream!