Skip to content

Commit c100af6

Browse files
till-varoquauxilevkivskyi
authored andcommitted
Initial import of PEP 593: Flexible function and variable annotations (#1014)
1 parent 502c87c commit c100af6

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

‎pep-0593.rst‎

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
PEP: 593
2+
Title: Flexible function and variable annotations
3+
Author: Till Varoquaux <till@fb.com>, Konstantin Kashin <kkashin@fb.com>
4+
Sponsor: Ivan Levkivskyi <levkivskyi@gmail.com>
5+
Discussions-To: typing-sig@python.org
6+
Status: Draft
7+
Type: Standards Track
8+
Content-Type: text/x-rst
9+
Created: 26-April-2019
10+
Python-Version:
11+
Post-History:
12+
13+
Abstract
14+
--------
15+
16+
This PEP introduces a mechanism to extend the type annotations from PEP
17+
484 with arbitrary metadata.
18+
19+
Motivation
20+
----------
21+
22+
PEP 484 provides a standard semantic for the annotations introduced in
23+
PEP 3107. PEP 484 is prescriptive but it is the de-facto standard
24+
for most of the consumers of annotations; in many statically checked
25+
code bases, where type annotations are widely used, they have
26+
effectively crowded out any other form of annotation. Some of the use
27+
cases for annotations described in PEP 3107 (database mapping,
28+
foreign languages bridge) are not currently realistic given the
29+
prevalence of type annotations. Furthermore the standardisation of type
30+
annotations rules out advanced features only supported by specific type
31+
checkers.
32+
33+
Rationale
34+
---------
35+
36+
This PEP adds an ``Annotated`` type to the typing module to decorate
37+
existing types with context-specific metadata. Specifically, a type
38+
``T`` can be annotated with metadata ``x`` via the typehint
39+
``Annotated[T, x]``. This metadata can be used for either static
40+
analysis or at runtime. If a library (or tool) encounters a typehint
41+
``Annotated[T, x]`` and has no special logic for metadata ``x``, it
42+
should ignore it and simply treat the type as ``T``. Unlike the
43+
``no_type_check`` functionality that currently exists in the ``typing``
44+
module which completely disables typechecking annotations on a function
45+
or a class, the ``Annotated`` type allows for both static typechecking
46+
of ``T`` (e.g., via mypy [mypy]_ or Pyre [pyre]_, which can safely ignore ``x``)
47+
together with runtime access to ``x`` within a specific application. The
48+
introduction of this type would address a diverse set of use cases of interest
49+
to the broader Python community.
50+
51+
This was originally brought up as issue 600 [issue-600]_ in the typing github
52+
and then discussed in Python ideas [python-ideas]_.
53+
54+
Motivating examples
55+
-------------------
56+
57+
Combining runtime and static uses of annotations
58+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59+
60+
There's an emerging trend of libraries leveraging the typing annotations at
61+
runtime (e.g.: dataclasses); having the ability to extend the typing annotations
62+
with external data would be a great boon for those libraries.
63+
64+
Here's an example of how a hypothetical module could leverage annotations to
65+
read c structs::
66+
67+
UnsignedShort = Annotated[int, struct2.ctype('H')]
68+
SignedChar = Annotated[int, struct2.ctype('b')]
69+
70+
class Student(struct2.Packed):
71+
# mypy typechecks 'name' field as 'str'
72+
name: Annotated[str, struct2.ctype("<10s")]
73+
serialnum: UnsignedShort
74+
school: SignedChar
75+
76+
# 'unpack' only uses the metadata within the type annotations
77+
Student.unpack(record)
78+
# Student(name=b'raymond ', serialnum=4658, school=264)
79+
80+
Lowering barriers to developing new typing constructs
81+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82+
83+
Typically when adding a new type, a developer need to upstream that type to the
84+
typing module and change mypy, PyCharm [pycharm]_, Pyre, pytype [pytype]_,
85+
etc...
86+
This is particularly important when working on open-source code that
87+
makes use of these types, seeing as the code would not be immediately
88+
transportable to other developers' tools without additional logic. As a result,
89+
there is a high cost to developing and trying out new types in a codebase.
90+
Ideally, authors should be able to introduce new types in a manner that allows
91+
for graceful degradation (e.g.: when clients do not have a custom mypy plugin
92+
[mypy-plugin]_), which would lower the barrier to development and ensure some
93+
degree of backward compatibility.
94+
95+
For example, suppose that an author wanted to add support for tagged unions
96+
[tagged-union]_ to Python. One way to accomplish would be to annotate
97+
``TypedDict`` [typed-dict]_ in Python such that only one field is allowed to be
98+
set::
99+
100+
Currency = Annotated[
101+
TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),
102+
TaggedUnion,
103+
]
104+
105+
This is a somewhat cumbersome syntax but it allows us to iterate on this
106+
proof-of-concept and have people with type checkers (or other tools) that don't
107+
yet support this feature work in a codebase with tagged unions. The author could
108+
easily test this proposal and iron out the kinks before trying to upstream tagged
109+
union to ``typing``, mypy, etc. Moreover, tools that do not have support for
110+
parsing the ``TaggedUnion`` annotation would still be able able to treat
111+
``Currency`` as a ``TypedDict``, which is still a close approximation (slightly
112+
less strict).
113+
114+
Specification
115+
-------------
116+
117+
Syntax
118+
~~~~~~
119+
120+
``Annotated`` is parameterized with a type and an arbitrary list of
121+
Python values that represent the annotations. Here are the specific
122+
details of the syntax:
123+
124+
* The first argument to ``Annotated`` must be a valid type
125+
126+
* Multiple type annotations are supported (``Annotated`` supports variadic
127+
arguments)::
128+
129+
Annotated[int, ValueRange(3, 10), ctype("char")]
130+
131+
* ``Annotated`` must be called with at least two arguments (
132+
``Annotated[int]`` is not valid)
133+
134+
* The order of the annotations is preserved and matters for equality
135+
checks::
136+
137+
Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[
138+
int, ctype("char"), ValueRange(3, 10)
139+
]
140+
141+
* Nested ``Annotated`` types are flattened, with metadata ordered
142+
starting with the innermost annotation::
143+
144+
Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
145+
int, ValueRange(3, 10), ctype("char")
146+
]
147+
148+
* Duplicated annotations are not removed::
149+
150+
Annotated[int, ValueRange(3, 10)] != Annotated[
151+
int, ValueRange(3, 10), ValueRange(3, 10)
152+
]
153+
154+
* ``Annotated`` can be used with nested and generic aliases::
155+
156+
Typevar T = ...
157+
Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]
158+
V = Vec[int]
159+
160+
V == Annotated[List[Tuple[int, int]], MaxLen(10)]
161+
162+
Consuming annotations
163+
~~~~~~~~~~~~~~~~~~~~~
164+
165+
Ultimately, the responsibility of how to interpret the annotations (if
166+
at all) is the responsibility of the tool or library encountering the
167+
``Annotated`` type. A tool or library encountering an ``Annotated`` type
168+
can scan through the annotations to determine if they are of interest
169+
(e.g., using ``isinstance()``).
170+
171+
**Unknown annotations:** When a tool or a library does not support
172+
annotations or encounters an unknown annotation it should just ignore it
173+
and treat annotated type as the underlying type. For example, when encountering
174+
an annotation that is not an instance of ``struct2.ctype`` to the annotations
175+
for name (e.g., ``Annotated[str, 'foo', struct2.ctype("<10s")]``), the unpack
176+
method should ignore it.
177+
178+
**Namespacing annotations:** Namespaces are not needed for annotations since
179+
the class used by the annotations acts as a namespace.
180+
181+
**Multiple annotations:** It's up to the tool consuming the annotations
182+
to decide whether the client is allowed to have several annotations on
183+
one type and how to merge those annotations.
184+
185+
Since the ``Annotated`` type allows you to put several annotations of
186+
the same (or different) type(s) on any node, the tools or libraries
187+
consuming those annotations are in charge of dealing with potential
188+
duplicates. For example, if you are doing value range analysis you might
189+
allow this::
190+
191+
T1 = Annotated[int, ValueRange(-10, 5)]
192+
T2 = Annotated[T1, ValueRange(-20, 3)]
193+
194+
Flattening nested annotations, this translates to::
195+
196+
T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]
197+
198+
Interaction with ``get_type_hints()``
199+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
200+
201+
``typing.get_type_hints()`` will take a new argument ``include_extras`` that
202+
defaults to ``False`` to preserve backward compatibility. When
203+
``include_extras`` is ``False``, the extra annotations will be stripped
204+
out of the returned value. Otherwise, the annotations will be returned
205+
unchanged::
206+
207+
@struct2.packed
208+
class Student(NamedTuple):
209+
name: Annotated[str, struct.ctype("<10s")]
210+
211+
get_type_hints(Student) == {'name': str}
212+
get_type_hints(Student, include_extras=False) == {'name': str}
213+
get_type_hints(Student, include_extras=True) == {
214+
'name': Annotated[str, struct.ctype("<10s")]
215+
}
216+
217+
Aliases & Concerns over verbosity
218+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
219+
220+
Writing ``typing.Annotated`` everywhere can be quite verbose;
221+
fortunately, the ability to alias annotations means that in practice we
222+
don't expect clients to have to write lots of boilerplate code::
223+
224+
T = TypeVar('T')
225+
Const = Annotated[T, my_annotations.CONST]
226+
227+
Class C:
228+
def const_method(self: Const[List[int]]) -> int:
229+
...
230+
231+
Rejected ideas
232+
--------------
233+
234+
Some of the proposed ideas were rejected from this PEP because they would
235+
cause ``Annotated`` to not integrate cleanly with the other typing annotations:
236+
237+
* ``Annotated`` cannot infer the decorated type. You could imagine that
238+
``Annotated[..., Immutable]`` could be used to mark a value as immutable
239+
while still infering its type. Typing does not support support using the
240+
inferred type anywhere else [issue-276]_; it's best to not add this as a
241+
special case.
242+
243+
* Using ``(Type, Ann1, Ann2, ...)`` instead of
244+
``Annotated[Type, Ann1, Ann2, ...]``. This would cause confusion when
245+
annotations appear in nested positions (``Callable[[A, B], C]`` is too similar
246+
to ``Callable[[(A, B)], C]``) and would make it impossible for constructors to
247+
be passthrough (``T(5) == C(5)`` when ``C = Annotation[T, Ann]``).
248+
249+
This feature was left out to keep the design simple:
250+
251+
* ``Annotated`` cannot be called with a single argument. Annotated could support
252+
returning the underlying value when called with a single argument (e.g.:
253+
``Annotated[int] == int``). This complicates the specifications and adds
254+
little benefit.
255+
256+
257+
References
258+
----------
259+
260+
.. [issue-600]
261+
https://github.com/python/typing/issues/600
262+
263+
.. [python-ideas]
264+
https://mail.python.org/pipermail/python-ideas/2019-January/054908.html
265+
266+
.. [struct-doc]
267+
https://docs.python.org/3/library/struct.html#examples
268+
269+
.. [mypy]
270+
http://www.mypy-lang.org/
271+
272+
.. [pyre]
273+
https://pyre-check.org/
274+
275+
.. [pycharm]
276+
https://www.jetbrains.com/pycharm/
277+
278+
.. [pytype]
279+
https://github.com/google/pytype
280+
281+
.. [mypy-plugin]
282+
https://github.com/python/mypy_extensions
283+
284+
.. [tagged-union]
285+
https://en.wikipedia.org/wiki/Tagged_union
286+
287+
.. [typed-dict]
288+
https://mypy.readthedocs.io/en/latest/more_types.html#typeddict
289+
290+
.. [issue-276]
291+
https://github.com/python/typing/issues/276
292+
293+
Copyright
294+
---------
295+
296+
This document has been placed in the public domain.

0 commit comments

Comments
 (0)