PEP 593 add typing.Annotated (Flexible function and variable annotations)

Description

PEP 593 introduced an typing.Annotated type to decorate existing types with context-specific metadata and new include_extras parameter to typing.get_type_hints() to access the metadata at runtime. (Contributed by Till Varoquaux and Konstantin Kashin.)

Motivating examples

Combining runtime and static uses of annotations

There’s an emerging trend of libraries leveraging the typing annotations at runtime (e.g.: dataclasses); having the ability to extend the typing annotations with external data would be a great boon for those libraries.

Here’s an example of how a hypothetical module could leverage annotations to read c structs:

 1 from typing import Annotated
 2
 3 UnsignedShort = Annotated[int, struct2.ctype('H')]
 4 SignedChar = Annotated[int, struct2.ctype('b')]
 5
 6 class Student(struct2.Packed):
 7     # mypy typechecks 'name' field as 'str'
 8     name: Annotated[str, struct2.ctype("<10s")]
 9     serialnum: UnsignedShort
10     school: SignedChar
11
12 # 'unpack' only uses the metadata within the type annotations
13 Student.unpack(record)
14 # Student(name=b'raymond   ', serialnum=4658, school=264)

Lowering barriers to developing new typing constructs

Typically when adding a new type, a developer need to upstream that type to the typing module and change mypy, PyCharm [pycharm], Pyre, pytype [pytype], etc…

This is particularly important when working on open-source code that makes use of these types, seeing as the code would not be immediately transportable to other developers’ tools without additional logic.

As a result, there is a high cost to developing and trying out new types in a codebase .

Ideally, authors should be able to introduce new types in a manner that allows for graceful degradation (e.g.: when clients do not have a custom mypy plugin [mypy-plugin]), which would lower the barrier to development and ensure some degree of backward compatibility.

For example, suppose that an author wanted to add support for tagged unions [tagged-union] to Python. One way to accomplish would be to annotate TypedDict [typed-dict] in Python such that only one field is allowed to be set:

1 from typing import Annotated
2
3 Currency = Annotated[
4     TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),
5     TaggedUnion,
6 ]

This is a somewhat cumbersome syntax but it allows us to iterate on this proof-of-concept and have people with type checkers (or other tools) that don’t yet support this feature work in a codebase with tagged unions.

The author could easily test this proposal and iron out the kinks before trying to upstream tagged union to typing, mypy, etc.

Moreover, tools that do not have support for parsing the TaggedUnion annotation would still be able able to treat Currency as a TypedDict, which is still a close approximation (slightly less strict).

Syntax

Annotated is parameterized with a type and an arbitrary list of Python values that represent the annotations .

Here are the specific details of the syntax :

  • The first argument to Annotated must be a valid type

  • Multiple type annotations are supported ( Annotated supports variadic arguments ):

    Annotated[int, ValueRange(3, 10), ctype("char")]
    
  • Annotated must be called with at least two arguments ( Annotated[int] is not valid)

  • The order of the annotations is preserved and matters for equality checks:

    Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[
        int, ctype("char"), ValueRange(3, 10)
    ]
    
  • Nested Annotated types are flattened, with metadata ordered starting with the innermost annotation:

    Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
        int, ValueRange(3, 10), ctype("char")
    ]
    
  • Duplicated annotations are not removed:

    Annotated[int, ValueRange(3, 10)] != Annotated[
        int, ValueRange(3, 10), ValueRange(3, 10)
    ]
    
  • Annotated can be used with nested and generic aliases:

    Typevar T = ...
    Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]
    V = Vec[int]
    
    V == Annotated[List[Tuple[int, int]], MaxLen(10)]