Skip to content

Commit 2756e19

Browse files
author
Till Varoquaux
committed
Initial import of PEP 592
1 parent 6042e32 commit 2756e19

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed

pep-0592.rst

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
PEP: 592
2+
Title: External annotations in the typing module
3+
Author: Till Varoquaux <[email protected]>, Konstantin Kashin <[email protected]>
4+
Sponsor: Ivan Levkivskyi <[email protected]>
5+
Discussions-To: [email protected]
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+
We propose adding 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 current 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. We
48+
believe that the introduction of this type would address a diverse set
49+
of use cases of interest 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+
Example::
65+
66+
UnsignedShort = Annotated[int, cstruct.ctype('H')]
67+
SignedChar = Annotated[int, cstruct.ctype('b')]
68+
69+
class Student(cstruct.Packed):
70+
# mypy typechecks 'name' field as 'str'
71+
name: Annotated[str, cstruct.ctype("<10s")]
72+
serialnum: UnsignedShort
73+
school: SignedChar
74+
gradelevel: 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, gradelevel=8)
79+
80+
Lowering barriers to developing new typing constructs
81+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82+
83+
Typically when adding a new type, we need to upstream that type to the
84+
typing module and change mypy, PyCharm [pycharm]_, Pyre,
85+
pytype [pytype]_, etc...
86+
This is particularly important when working on open-source code that
87+
makes use of our new 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, we should be able to introduce new types in a manner that allows for
91+
graceful degradation 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 we 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. We could easily
108+
test this proposal and iron out the kinks before trying to upstream tagged union
109+
to ``typing``, mypy, etc. Moreover, tools that do not have support for parsing
110+
the ``TaggedUnion`` annotation would still be able able to treat ``Currency`` as
111+
a ``TypedDict``, which is still a close approximation (slightly less strict).
112+
113+
Specification
114+
-------------
115+
116+
Syntax
117+
~~~~~~
118+
119+
``Annotated`` is parameterized with a type and an arbitrary list of
120+
Python values that represent the annotations. Here are the specific
121+
details of the syntax:
122+
123+
* The first argument to ``Annotated`` must be a valid type
124+
125+
* Multiple type annotations are supported (``Annotated`` supports variadic
126+
arguments)::
127+
128+
Annotated[int, ValueRange(3, 10), ctype("char")]
129+
130+
* ``Annotated`` must be called with at least two arguments (
131+
``Annotated[int]`` is not valid)
132+
133+
* The order of the annotations is preserved and matters for equality
134+
checks::
135+
136+
Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[
137+
int, ctype("char"), ValueRange(3, 10)
138+
]
139+
140+
* Nested ``Annotated`` types are flattened, with metadata ordered
141+
starting with the innermost annotation::
142+
143+
Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
144+
int, ValueRange(3, 10), ctype("char")
145+
]
146+
147+
* Duplicated annotations are not removed::
148+
149+
Annotated[int, ValueRange(3, 10)] != Annotated[
150+
int, ValueRange(3, 10), ValueRange(3, 10)
151+
]
152+
153+
* ``Annotated`` can be used as a higher order aliases::
154+
155+
Typevar T = ...
156+
Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]
157+
V = Vec[int]
158+
159+
V == Annotated[List[Tuple[int, int]], MaxLen(10)]
160+
161+
Consuming annotations
162+
~~~~~~~~~~~~~~~~~~~~~
163+
164+
Ultimately, the responsibility of how to interpret the annotations (if
165+
at all) is the responsibility of the tool or library encountering the
166+
``Annotated`` type. A tool or library encountering an ``Annotated`` type
167+
can scan through the annotations to determine if they are of interest
168+
(e.g., using ``isinstance()``).
169+
170+
**Unknown annotations:** When a tool or a library does not support
171+
annotations or encounters an unknown annotation it should just ignore it
172+
and treat annotated type as the underlying type. For example, if we were
173+
to add an annotation that is not an instance of ``new_struct.ctype`` to the
174+
annotation for name (e.g.,
175+
``Annotated[str, 'foo', new_struct.ctype("<10s")]``), the unpack method
176+
should ignore it.
177+
178+
**Namespacing annotations:** We do not need namespaces for annotations
179+
since 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+
@struct.packedclass Student(NamedTuple):
208+
name: Annotated[str, struct.ctype("<10s")]
209+
210+
get_type_hints(Student) == {'name': str}
211+
get_type_hints(Student, include_extras=False) == {'name': str}
212+
get_type_hints(Student, include_extras=True) == {
213+
'name': Annotated[str, struct.ctype("<10s")]
214+
}
215+
216+
Aliases & Concerns over verbosity
217+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
218+
219+
Writing ``typing.Annotated`` everywhere can be quite verbose;
220+
fortunately, the ability to alias annotations means that in practice we
221+
don't expect clients to have to write lots of boilerplate code::
222+
223+
T = TypeVar('T')
224+
Const = Annotated[T, my_annotations.CONST]
225+
226+
Class C:
227+
def const_method(self: Const[List[int]]) -> int:
228+
...
229+
230+
Rejected ideas
231+
--------------
232+
233+
Some of the proposed ideas were rejected from this PEP because they would
234+
cause ``Annotated`` to not integrate cleanly with the other typing annotations:
235+
236+
* ``Annotated`` cannot infer the decorated type. You could imagine that
237+
``Annotated[..., Immutable]`` could be used to mark a value as immutable
238+
while still infering its type. Typing does not support support using the
239+
inferred type anywhere else [issue-276]_; it's best to not add this as a
240+
special case.
241+
242+
* We could use ``(Type, Ann1, Ann2, ...)`` instead of
243+
``Annotated[Type, Ann1, Ann2, ...]``. This would cause confusion when
244+
annotations appear in nested positions (``Callable[[A, B], C]`` is too similar
245+
to ``Callable[[(A, B)], C]``) and would make it impossible for constructors to
246+
be passthrough (``T(5) == C(5)`` when ``C = Annotation[T, Ann]``).
247+
248+
This feature was left out to keep the design simple:
249+
250+
* ``Annotated`` cannot be called with a single argument. Annotated could support
251+
returning the underlying value when called with a single argument (e.g.:
252+
``Annotated[int] == int``). This complicates the specifications and adds
253+
little benefit.
254+
255+
256+
References
257+
----------
258+
259+
.. [issue-600]
260+
https://github.com/python/typing/issues/600
261+
262+
.. [python-ideas]
263+
https://mail.python.org/pipermail/python-ideas/2019-January/054908.html
264+
265+
.. [struct-doc]
266+
https://docs.python.org/3/library/struct.html#examples
267+
268+
.. [mypy]
269+
http://www.mypy-lang.org/
270+
271+
.. [pyre]
272+
https://pyre-check.org/
273+
274+
.. [pycharm]
275+
https://www.jetbrains.com/pycharm/
276+
277+
.. [pytype]
278+
https://github.com/google/pytype
279+
280+
.. [mypy-plugin]
281+
https://github.com/python/mypy_extensions
282+
283+
.. [tagged-union]
284+
https://en.wikipedia.org/wiki/Tagged_union
285+
286+
.. [typed-dict]
287+
https://mypy.readthedocs.io/en/latest/more_types.html#typeddict
288+
289+
.. [issue-276]
290+
https://github.com/python/typing/issues/276
291+
292+
Copyright
293+
---------
294+
295+
This document has been placed in the public domain.

0 commit comments

Comments
 (0)