|
| 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 | + |
| 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