<div dir="ltr"><div dir="ltr"><br></div><div class="gmail_quote"><div dir="ltr">On Thu, Jan 17, 2019 at 2:34 PM Till <<a href="mailto:till.varoquaux@gmail.com">till.varoquaux@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div><font color="#222222">We started a discussion in <a href="https://github.com/python/typing/issues/600" target="_blank">https://github.com/python/typing/issues/600</a> about adding support for extra annotations in the typing module.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Since this is probably going to turn into a PEP I'm transferring the discussion here to have more visibility. </font></div><div><font color="#222222"><br></font></div><div><font color="#222222">The document below has been modified a bit from the one in GH to reflect the feedback I got: </font></div><div><font color="#222222"><br></font></div><div><font color="#222222"> + Added a small blurb about how ``Annotated`` should support being used as an alias </font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Things that were raised but are not reflected in this document:</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"> + The dataclass example is confusing. I kept it for now because dataclasses often come up in conversations about why we might want to support annotations in the typing module. Maybe I should rework the section. </font></div><div><font color="#222222"><br></font></div><div><font color="#222222"> + `...` as a valid parameter for the first argument (if you want to add an annotation but use the type inferred by your type checker). This is an interesting idea, it's probably worth adding support for it if and only if we decide to support in other places. (c.f.: <a href="https://github.com/python/typing/issues/276" target="_blank">https://github.com/python/typing/issues/276</a>)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Thanks,</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Add support for external annotations in the typing module</font></div><div><font color="#222222">==========================================================</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">We propose adding an ``Annotated`` type to the typing module to decorate existing types with context-specific metadata. 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. 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 current 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`` (e.g., via MyPy or Pyre, which can safely ignore ``x``)  together with runtime access to ``x`` within a specific application. We believe that the introduction of this type would address a diverse set of use cases of interest to the broader Python community.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Motivating examples:</font></div><div><font color="#222222">~~~~~~~~~~~~~~~~~~~~</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">reading binary data</font></div><div><font color="#222222">+++++++++++++++++++</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">The ``struct`` module provides a way to read and write C structs directly from their byte representation. It currently relies on a string representation of the C type to read in values::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  record = b'raymond   \x32\x12\x08\x01\x08'</font></div><div><font color="#222222">  name, serialnum, school, gradelevel = unpack('<10sHHb', record)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">The struct documentation [struct-examples]_ suggests using a named tuple to unpack the values and make this a bit more tractable::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  from collections import namedtuple</font></div><div><font color="#222222">  Student = namedtuple('Student', 'name serialnum school gradelevel')</font></div><div><font color="#222222">  Student._make(unpack('<10sHHb', record))</font></div><div><font color="#222222">  # Student(name=b'raymond   ', serialnum=4658, school=264, gradelevel=8)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">However, this recommendation is somewhat problematic; as we add more fields, it's going to get increasingly tedious to match the properties in the named tuple with the arguments in ``unpack``.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Instead, annotations can provide better interoperability with a type checker or an IDE without adding any special logic outside of the ``struct`` module::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  from typing import NamedTuple</font></div><div><font color="#222222">  UnsignedShort = Annotated[int, struct.ctype('H')]</font></div><div><font color="#222222">  SignedChar = Annotated[int, struct.ctype('b')]</font></div><div><font color="#222222">  </font></div><div><font color="#222222">  @struct.packed</font></div><div><font color="#222222">  class Student(NamedTuple):</font></div><div><font color="#222222">    # MyPy typechecks 'name' field as 'str'</font></div><div><font color="#222222">    name: Annotated[str, struct.ctype("<10s")]</font></div><div><font color="#222222">    serialnum: UnsignedShort</font></div><div><font color="#222222">    school: SignedChar</font></div><div><font color="#222222">    gradelevel: SignedChar</font></div><div><font color="#222222">  </font></div><div><font color="#222222">  # 'unpack' only uses the metadata within the type annotations</font></div><div><font color="#222222">  Student.unpack(record))</font></div><div><font color="#222222">  # Student(name=b'raymond   ', serialnum=4658, school=264, gradelevel=8)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">dataclasses</font></div><div><font color="#222222">++++++++++++</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Here's an example with dataclasses [dataclass]_ that is a problematic from the typechecking standpoint::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  from dataclasses import dataclass, field</font></div><div><font color="#222222">  </font></div><div><font color="#222222">  @dataclass</font></div><div><font color="#222222">  class C:</font></div><div><font color="#222222">    myint: int = 0</font></div><div><font color="#222222">    # the field tells the @dataclass decorator that the default action in the</font></div><div><font color="#222222">    # constructor of this class is to set "self.mylist = list()"</font></div><div><font color="#222222">    mylist: List[int] = field(default_factory=list)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Even though one might expect that ``mylist`` is a class attribute accessible via ``C.mylist`` (like ``C.myint`` is) due to the assignment syntax, that is not the case. Instead, the ``@dataclass`` decorator strips out the assignment to this attribute, leading to an ``AttributeError`` upon access::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  C.myint  # Ok: 0</font></div><div><font color="#222222">  C.mylist  # AttributeError: type object 'C' has no attribute 'mylist'</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">This can lead to confusion for newcomers to the library who may not expect this behavior. Furthermore, the typechecker needs to understand the semantics of dataclasses and know to not treat the above example as an assignment operation in (which translates to additional complexity).</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">It makes more sense to move the information contained in ``field`` to an annotation::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  @dataclass</font></div><div><font color="#222222">  class C:</font></div><div><font color="#222222">      myint: int = 0</font></div><div><font color="#222222">      mylist: Annotated[List[int], field(default_factory=list)]</font></div><div><font color="#222222">  </font></div><div><font color="#222222">  # now, the AttributeError is more intuitive because there is no assignment operator</font></div><div><font color="#222222">  C.mylist  # AttributeError</font></div><div><font color="#222222">  </font></div><div><font color="#222222">  # the constructor knows how to use the annotations to set the 'mylist' attribute </font></div><div><font color="#222222">  c = C()</font></div><div><font color="#222222">  c.mylist  # []</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">The main benefit of writing annotations like this is that it provides a way for clients to gracefully degrade when they don't know what to do with the extra annotations (by just ignoring them). If you used a typechecker that didn't have any special handling for dataclasses and the ``field`` annotation, you would still be able to run checks as though the type were simply::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  class C:</font></div><div><font color="#222222">      myint: int = 0</font></div><div><font color="#222222">      mylist: List[int]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">lowering barriers to developing new types</font></div><div><font color="#222222">+++++++++++++++++++++++++++++++++++++++++</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Typically when adding a new type, we need to upstream that type to the typing module and change MyPy [MyPy]_, PyCharm [PyCharm]_, Pyre [Pyre]_, pytype [pytype]_, etc. This is particularly important when working on open-source code that makes use of our new types, seeing as the code would not be immediately transportable to other developers' tools without additional logic (this is a limitation of MyPy plugins [MyPy-plugins]_), which allow for extending MyPy but would require a consumer of new typehints to be using MyPy and have the same plugin installed). As a result, there is a high cost to developing and trying out new types in a codebase. Ideally, we should be able to introduce new types in a manner that allows for graceful degradation when clients do not have a custom MyPy plugin, which would lower the barrier to development and ensure some degree of backward compatibility.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">For example, suppose that we wanted to add support for tagged unions [tagged-unions]_ to Python. One way to accomplish would be to annotate ``TypedDict`` in Python such that only one field is allowed to be set::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  Currency = Annotated(</font></div><div><font color="#222222">    TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),</font></div><div><font color="#222222">    TaggedUnion,</font></div><div><font color="#222222">  )</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">This is a somewhat cumbersome syntax but it allows us to iterate on this proof-of-concept and have people with non-patched IDEs work in a codebase with tagged unions. We 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).</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">Details of proposed changes to ``typing``</font></div><div><font color="#222222">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">syntax</font></div><div><font color="#222222">++++++</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">``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:</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* The first argument to ``Annotated`` must be a valid ``typing`` type or ``...`` (to use the infered type).</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* Multiple type annotations are supported (Annotated supports variadic arguments): ``Annotated[int, ValueRange(3, 10), ctype("char")]``</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* ``Annotated`` must be called with at least two arguments (``Annotated[int]`` is not valid)</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* The order of the annotations is preserved and matters for equality checks::</font></div><div><font color="#222222">  </font></div><div><font color="#222222">   Annotated[int, ValueRange(3, 10), ctype("char")] != \</font></div><div><font color="#222222">    Annotated[int, ctype("char"), ValueRange(3, 10)]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* Nested ``Annotated`` types are flattened, with metadata ordered starting with the innermost annotation::</font></div><div><font color="#222222">  </font></div><div><font color="#222222">   Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] ==\</font></div><div><font color="#222222">    Annotated[int, ValueRange(3, 10), ctype("char")]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* Duplicated annotations are not removed: ``Annotated[int, ValueRange(3, 10)] != Annotated[int, ValueRange(3, 10), ValueRange(3, 10)]``</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">* ``Annotation`` can be used a higher order aliases::</font></div><div><font color="#222222">  </font></div><div><font color="#222222">    Typevar T = ...</font></div><div><font color="#222222">    Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]</font></div><div><font color="#222222">    # Vec[int] == `Annotated[List[Tuple[int, int]], MaxLen(10)]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222"><br></font></div><div><font color="#222222">consuming annotations</font></div><div><font color="#222222">++++++++++++++++++++++</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">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`).</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">**Unknown annotations**</font></div><div><font color="#222222">  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. For example, if we were to add an annotation that is not an instance of `struct.ctype` to the annotation for name (e.g., `Annotated[str, 'foo', struct.ctype("<10s")]`), the unpack method should ignore it.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">**Namespacing annotations**</font></div><div><font color="#222222">  We do not need namespaces for annotations since the class used by the annotations acts as a namespace.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">**Multiple annotations**</font></div><div><font color="#222222">  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.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  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::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">    T1 = Annotated[int, ValueRange(-10, 5)]</font></div><div><font color="#222222">    T2 = Annotated[T1, ValueRange(-20, 3)]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  Flattening nested annotations, this translates to::</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">    T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  An application consuming this type might choose to reduce these annotations via an intersection of the ranges, in which case ``T2`` would be treated equivalently to ``Annotated[int, ValueRange(-10, 3)]``.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  An alternative application might reduce these via a union, in which case ``T2`` would be treated equivalently to ``Annotated[int, ValueRange(-20, 5)]``.</font></div><div><font color="#222222"><br></font></div><div><font color="#222222">  Other applications may decide to not support multiple annotations and throw an exception.</font></div></div></blockquote><div><br></div><div><div>(0) Observaton / TL;DR - This PEP really seems to be more of a way to declare multiple different arbitrary purposes annotations all attached to a single callable/parameter/return/variable.  So that static checkers continue to work, but runtime user of annotations for whatever purpose can also work at the same time.</div><br class="gmail-Apple-interchange-newline"></div><div><div>(1a) A struct.unpack supporting this will then need to evaluate annotations in the outer scope at runtime due to our desired long term PEP-563 `from __future__ import annotations` behavior.  But that becomes true of anything else wanting to use annotations at runtime so we should really make a typing library function that does this for everyone to use.</div></div><div><br></div><div>(1b) This proposal potentially expands the burden of type checkers... but it shouldn't.  They should be free to take the first type listed in an Annotated[] block as the type of the variable, raising an error if someone has listed multiple types (telling them to use Union[] for that).  a static checker <i>could</i> do useful things with multiple annotations it knows how to handle, but I think it'd be unwise to implement that in any manner where Annotated and Union could both be used for the same purpose.</div><div><br></div><div>It makes me wonder if Annotated[] is meaningfully different from Union at all.</div><div><br></div><div>(2a) At first glance I don't like that the `T1 = Annotated[int, SomeOtherInfo(23)]` syntax uses [] rather than () as it really is constructing a runtime type.  It isn't clear what should use [] and what should use () so I'd suggest using () for everything there.<br></div><div><br></div><div>(2b) Ask yourself: Why should SomeOtherInfo and ValueRange and struct.ctype be () calls yet none of `Annotated[Union[List[bytes], Dict[bytes, Optional[float]]]]` be calls?  If you can come up with an answer to that, why _should_ anyone need to know that?</div><div><br></div><div>-gps</div></div></div>