Skip to content

Commit

Permalink
[3.12] Further improve docs for typing.Annotated (pythonGH-105498) (p…
Browse files Browse the repository at this point in the history
…ython#105503)

Further improve docs for `typing.Annotated` (pythonGH-105498)
(cherry picked from commit d213c29)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
  • Loading branch information
miss-islington and AlexWaygood authored Jun 8, 2023
1 parent 2b6f475 commit 3c5f0ea
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 61 deletions.
132 changes: 77 additions & 55 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1194,40 +1194,36 @@ These can be used as types in annotations using ``[]``, each having a unique syn

.. data:: Annotated

A type, introduced in :pep:`593` (``Flexible function and variable
annotations``), to decorate existing types with context-specific metadata
(possibly multiple pieces of it, as ``Annotated`` is variadic).
Specifically, a type ``T`` can be annotated with metadata ``x`` via the
typehint ``Annotated[T, x]``. This metadata can be used for either static
analysis or at runtime: at runtime, it is stored in a :attr:`__metadata__`
attribute. If a library (or tool) encounters a typehint
``Annotated[T, x]`` and has no special logic for metadata ``x``, it
should ignore it and simply treat the type as ``T``. Unlike the
``no_type_check`` functionality that currently exists in the ``typing``
module which completely disables typechecking annotations on a function
or a class, the ``Annotated`` type allows for both static typechecking
of ``T`` (which can safely ignore ``x``)
together with runtime access to ``x`` within a specific application.

Ultimately, the responsibility of how to interpret the annotations (if
at all) is the responsibility of the tool or library encountering the
``Annotated`` type. A tool or library encountering an ``Annotated`` type
can scan through the annotations to determine if they are of interest
(e.g., using ``isinstance()``).

When a tool or a library does not support annotations or encounters an
unknown annotation it should just ignore it and treat annotated type as
the underlying type.

It's up to the tool consuming the annotations to decide whether the
client is allowed to have several annotations on one type and how to
merge those annotations.

Since the ``Annotated`` type allows you to put several annotations of
the same (or different) type(s) on any node, the tools or libraries
consuming those annotations are in charge of dealing with potential
duplicates. For example, if you are doing value range analysis you might
allow this:
Special typing form to add context-specific metadata to an annotation.

Add metadata ``x`` to a given type ``T`` by using the annotation
``Annotated[T, x]``. Metadata added using ``Annotated`` can be used by
static analysis tools or at runtime. At runtime, the metadata is stored
in a :attr:`!__metadata__` attribute.

If a library or tool encounters an annotation ``Annotated[T, x]`` and has
no special logic for the metadata, it should ignore the metadata and simply
treat the annotation as ``T``. As such, ``Annotated`` can be useful for code
that wants to use annotations for purposes outside Python's static typing
system.

Using ``Annotated[T, x]`` as an annotation still allows for static
typechecking of ``T``, as type checkers will simply ignore the metadata ``x``.
In this way, ``Annotated`` differs from the
:func:`@no_type_check <no_type_check>` decorator, which can also be used for
adding annotations outside the scope of the typing system, but
completely disables typechecking for a function or class.

The responsibility of how to interpret the metadata
lies with the the tool or library encountering an
``Annotated`` annotation. A tool or library encountering an ``Annotated`` type
can scan through the metadata elements to determine if they are of interest
(e.g., using :func:`isinstance`).

.. describe:: Annotated[<type>, <metadata>]

Here is an example of how you might use ``Annotated`` to add metadata to
type annotations if you were doing range analysis:

.. testcode::

Expand All @@ -1239,14 +1235,11 @@ These can be used as types in annotations using ``[]``, each having a unique syn
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]

Passing ``include_extras=True`` to :func:`get_type_hints` lets one
access the extra annotations at runtime.

The details of the syntax:
Details of the syntax:

* The first argument to ``Annotated`` must be a valid type

* Multiple type annotations are supported (``Annotated`` supports variadic
* Multiple metadata elements can be supplied (``Annotated`` supports variadic
arguments)::

@dataclass
Expand All @@ -1255,24 +1248,28 @@ These can be used as types in annotations using ``[]``, each having a unique syn

Annotated[int, ValueRange(3, 10), ctype("char")]

* ``Annotated`` must be called with at least two arguments (
It is up to the tool consuming the annotations to decide whether the
client is allowed to add multiple metadata elements to one annotation and how to
merge those annotations.

* ``Annotated`` must be subscripted with at least two arguments (
``Annotated[int]`` is not valid)

* The order of the annotations is preserved and matters for equality
* The order of the metadata elements is preserved and matters for equality
checks::

assert 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::
* Nested ``Annotated`` types are flattened. The order of the metadata elements
starts with the innermost annotation::

assert Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
int, ValueRange(3, 10), ctype("char")
]

* Duplicated annotations are not removed::
* Duplicated metadata elements are not removed::

assert Annotated[int, ValueRange(3, 10)] != Annotated[
int, ValueRange(3, 10), ValueRange(3, 10)
Expand All @@ -1292,21 +1289,46 @@ These can be used as types in annotations using ``[]``, each having a unique syn
# ``Annotated[list[tuple[int, int]], MaxLen(10)]``:
type V = Vec[int]

.. attribute:: __metadata__
* ``Annotated`` cannot be used with an unpacked :class:`TypeVarTuple`::

At runtime, the metadata associated with an ``Annotated`` type can be
retrieved via the ``__metadata__`` attribute.
type Variadic[*Ts] = Annotated[*Ts, Ann1] # NOT valid

For example:
This would be equivalent to::

.. doctest::
Annotated[T1, T2, T3, ..., Ann1]

where ``T1``, ``T2``, etc. are :class:`TypeVars <TypeVar>`. This would be
invalid: only one type should be passed to Annotated.

* By default, :func:`get_type_hints` strips the metadata from annotations.
Pass ``include_extras=True`` to have the metadata preserved:

.. doctest::

>>> from typing import Annotated, get_type_hints
>>> def func(x: Annotated[int, "metadata"]) -> None: pass
...
>>> get_type_hints(func)
{'x': <class 'int'>, 'return': <class 'NoneType'>}
>>> get_type_hints(func, include_extras=True)
{'x': typing.Annotated[int, 'metadata'], 'return': <class 'NoneType'>}

* At runtime, the metadata associated with an ``Annotated`` type can be
retrieved via the :attr:`!__metadata__` attribute:

.. doctest::

>>> from typing import Annotated
>>> X = Annotated[int, "very", "important", "metadata"]
>>> X
typing.Annotated[int, 'very', 'important', 'metadata']
>>> X.__metadata__
('very', 'important', 'metadata')

.. seealso::

>>> from typing import Annotated
>>> X = Annotated[int, "very", "important", "metadata"]
>>> X
typing.Annotated[int, 'very', 'important', 'metadata']
>>> X.__metadata__
('very', 'important', 'metadata')
:pep:`593` - Flexible function and variable annotations
The PEP introducing ``Annotated`` to the standard library.

.. versionadded:: 3.9

Expand Down
14 changes: 8 additions & 6 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2021,7 +2021,7 @@ class Annotated:
assert Annotated[int, '$'].__metadata__ == ('$',)
- Nested Annotated are flattened::
- Nested Annotated types are flattened::
assert Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
Expand All @@ -2032,15 +2032,17 @@ class Annotated:
- Annotated can be used as a generic type alias::
Optimized = Annotated[T, runtime.Optimize()]
assert Optimized[int] == Annotated[int, runtime.Optimize()]
type Optimized[T] = Annotated[T, runtime.Optimize()]
# type checker will treat Optimized[int]
# as equivalent to Annotated[int, runtime.Optimize()]
OptimizedList = Annotated[List[T], runtime.Optimize()]
assert OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
type OptimizedList[T] = Annotated[list[T], runtime.Optimize()]
# type checker will treat OptimizedList[int]
# as equivalent to Annotated[list[int], runtime.Optimize()]
- Annotated cannot be used with an unpacked TypeVarTuple::
Annotated[*Ts, Ann1] # NOT valid
type Variadic[*Ts] = Annotated[*Ts, Ann1] # NOT valid
This would be equivalent to::
Expand Down

0 comments on commit 3c5f0ea

Please sign in to comment.