Repository navigation
Initial import of PEP 593: Flexible function and variable annotations #1014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,295 @@ | ||||||
| PEP: 592 | ||||||
| Title: External annotations in the typing module | ||||||
| Author: Till Varoquaux <till@fb.com>, Konstantin Kashin <kkashin@fb.com> | ||||||
| Sponsor: Ivan Levkivskyi <levkivskyi@gmail.com> | ||||||
| Discussions-To: typing-sig@python.org | ||||||
| Status: Draft | ||||||
| Type: Standards Track | ||||||
| Content-Type: text/x-rst | ||||||
| Created: 26-April-2019 | ||||||
| Python-Version: | ||||||
| Post-History: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add |
||||||
|
|
||||||
| Abstract | ||||||
| -------- | ||||||
|
|
||||||
| This PEP introduces a mechanism to extend the type annotations from PEP | ||||||
| 484 with arbitrary metadata. | ||||||
|
|
||||||
| Motivation | ||||||
| ---------- | ||||||
|
|
||||||
| PEP 484 provides a standard semantic for the annotations introduced in | ||||||
| PEP 3107. PEP 484 is prescriptive but it is the de-facto standard | ||||||
| for most of the consumers of annotations; in many statically checked | ||||||
| code bases, where type annotations are widely used, they have | ||||||
| effectively crowded out any other form of annotation. Some of the use | ||||||
| cases for annotations described in PEP 3107 (database mapping, | ||||||
| foreign languages bridge) are not currently realistic given the | ||||||
| prevalence of type annotations. Furthermore the standardisation of type | ||||||
| annotations rules out advanced features only supported by specific type | ||||||
| checkers. | ||||||
|
|
||||||
| Rationale | ||||||
| --------- | ||||||
|
|
||||||
| 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`` | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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 [mypy]_ or Pyre [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. | ||||||
|
|
||||||
| This was originally brought up as issue 600 [issue-600]_ in the typing github | ||||||
| and then discussed in Python ideas [python-ideas]_. | ||||||
|
|
||||||
| Motivating examples | ||||||
| ------------------- | ||||||
|
|
||||||
| Combining runtime and static uses of annotations | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|
|
||||||
| There's an emerging trend of libraries leveraging the typing annotations at | ||||||
| runtime (e.g.: dataclasses); having the ability to extend the typing annotations | ||||||
| with external data would be a great boon for those libraries. | ||||||
|
|
||||||
| Example:: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make it clear that this example is about hypothetic functionality in |
||||||
|
|
||||||
| UnsignedShort = Annotated[int, cstruct.ctype('H')] | ||||||
| SignedChar = Annotated[int, cstruct.ctype('b')] | ||||||
|
|
||||||
| class Student(cstruct.Packed): | ||||||
| # mypy typechecks 'name' field as 'str' | ||||||
| name: Annotated[str, cstruct.ctype("<10s")] | ||||||
| serialnum: UnsignedShort | ||||||
| school: SignedChar | ||||||
| gradelevel: SignedChar | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can keep only one |
||||||
|
|
||||||
| # 'unpack' only uses the metadata within the type annotations | ||||||
| Student.unpack(record) | ||||||
| # Student(name=b'raymond ', serialnum=4658, school=264, gradelevel=8) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example (and to certain extent the second example) is unnecessarily detailed. You focus too much on advocating for specific use case, instead of explaining the big picture with the example. |
||||||
|
|
||||||
| Lowering barriers to developing new typing constructs | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|
|
||||||
| Typically when adding a new type, we need to upstream that type to the | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here term |
||||||
| typing module and change mypy, PyCharm [pycharm]_, 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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| transportable to other developers' tools without additional logic. 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 | ||||||
| [mypy-plugin]_, which would lower the barrier to development and ensure some | ||||||
| degree of backward compatibility. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH, this sentence is still not very clear. In particular, it is not clear what is special about mypy here. Maybe add "for example" somewhere? |
||||||
|
|
||||||
| For example, suppose that we wanted to add support for tagged unions | ||||||
| [tagged-union]_ to Python. One way to accomplish would be to annotate | ||||||
| ``TypedDict`` [typed-dict]_ in Python such that only one field is allowed to be | ||||||
| set:: | ||||||
|
|
||||||
| Currency = Annotated[ | ||||||
| TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False), | ||||||
| TaggedUnion, | ||||||
| ] | ||||||
|
|
||||||
| This is a somewhat cumbersome syntax but it allows us to iterate on this | ||||||
| proof-of-concept and have people with type checkers (or other tools) that don't | ||||||
| yet support this feature work in a codebase with tagged unions. We could easily | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You often use "we" while it is not always totally clear whom do you mean. |
||||||
| 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). | ||||||
|
|
||||||
| Specification | ||||||
| ------------- | ||||||
|
|
||||||
| Syntax | ||||||
| ~~~~~~ | ||||||
|
|
||||||
| ``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: | ||||||
|
|
||||||
| * The first argument to ``Annotated`` must be a valid type | ||||||
|
|
||||||
| * Multiple type annotations are supported (``Annotated`` supports variadic | ||||||
| arguments):: | ||||||
|
|
||||||
| Annotated[int, ValueRange(3, 10), ctype("char")] | ||||||
|
|
||||||
| * ``Annotated`` must be called with at least two arguments ( | ||||||
| ``Annotated[int]`` is not valid) | ||||||
|
|
||||||
| * The order of the annotations is preserved and matters for equality | ||||||
| checks:: | ||||||
|
|
||||||
| 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:: | ||||||
|
|
||||||
| Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[ | ||||||
| int, ValueRange(3, 10), ctype("char") | ||||||
| ] | ||||||
|
|
||||||
| * Duplicated annotations are not removed:: | ||||||
|
|
||||||
| Annotated[int, ValueRange(3, 10)] != Annotated[ | ||||||
| int, ValueRange(3, 10), ValueRange(3, 10) | ||||||
| ] | ||||||
|
|
||||||
| * ``Annotated`` can be used as a higher order aliases:: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| Typevar T = ... | ||||||
| Vec = Annotated[List[Tuple[T, T]], MaxLen(10)] | ||||||
| V = Vec[int] | ||||||
|
|
||||||
| V == Annotated[List[Tuple[int, int]], MaxLen(10)] | ||||||
|
|
||||||
| Consuming annotations | ||||||
| ~~~~~~~~~~~~~~~~~~~~~ | ||||||
|
|
||||||
| 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()``). | ||||||
|
|
||||||
| **Unknown annotations:** 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 ``new_struct.ctype`` to the | ||||||
| annotation for name (e.g., | ||||||
| ``Annotated[str, 'foo', new_struct.ctype("<10s")]``), the unpack method | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use same name for the hypothetic module here and in the first motivating example. |
||||||
| should ignore it. | ||||||
|
|
||||||
| **Namespacing annotations:** We do not need namespaces for annotations | ||||||
| since the class used by the annotations acts as a namespace. | ||||||
|
|
||||||
| **Multiple annotations:** 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:: | ||||||
|
|
||||||
| T1 = Annotated[int, ValueRange(-10, 5)] | ||||||
| T2 = Annotated[T1, ValueRange(-20, 3)] | ||||||
|
|
||||||
| Flattening nested annotations, this translates to:: | ||||||
|
|
||||||
| T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)] | ||||||
|
|
||||||
| Interaction with ``get_type_hints()`` | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|
|
||||||
| ``typing.get_type_hints()`` will take a new argument ``include_extras`` that | ||||||
| defaults to ``False`` to preserve backward compatibility. When | ||||||
| ``include_extras`` is ``False``, the extra annotations will be stripped | ||||||
| out of the returned value. Otherwise, the annotations will be returned | ||||||
| unchanged:: | ||||||
|
|
||||||
| @struct.packedclass Student(NamedTuple): | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something is still wrong with the formatting here, this is a syntax error. |
||||||
| name: Annotated[str, struct.ctype("<10s")] | ||||||
|
|
||||||
| get_type_hints(Student) == {'name': str} | ||||||
| get_type_hints(Student, include_extras=False) == {'name': str} | ||||||
| get_type_hints(Student, include_extras=True) == { | ||||||
| 'name': Annotated[str, struct.ctype("<10s")] | ||||||
| } | ||||||
|
|
||||||
| Aliases & Concerns over verbosity | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|
|
||||||
| Writing ``typing.Annotated`` everywhere can be quite verbose; | ||||||
| fortunately, the ability to alias annotations means that in practice we | ||||||
| don't expect clients to have to write lots of boilerplate code:: | ||||||
|
|
||||||
| T = TypeVar('T') | ||||||
| Const = Annotated[T, my_annotations.CONST] | ||||||
|
|
||||||
| Class C: | ||||||
| def const_method(self: Const[List[int]]) -> int: | ||||||
| ... | ||||||
|
|
||||||
| Rejected ideas | ||||||
| -------------- | ||||||
|
|
||||||
| Some of the proposed ideas were rejected from this PEP because they would | ||||||
| cause ``Annotated`` to not integrate cleanly with the other typing annotations: | ||||||
|
|
||||||
| * ``Annotated`` cannot infer the decorated type. You could imagine that | ||||||
| ``Annotated[..., Immutable]`` could be used to mark a value as immutable | ||||||
| while still infering its type. Typing does not support support using the | ||||||
| inferred type anywhere else [issue-276]_; it's best to not add this as a | ||||||
| special case. | ||||||
|
|
||||||
| * We could use ``(Type, Ann1, Ann2, ...)`` instead of | ||||||
| ``Annotated[Type, Ann1, Ann2, ...]``. This would cause confusion when | ||||||
| annotations appear in nested positions (``Callable[[A, B], C]`` is too similar | ||||||
| to ``Callable[[(A, B)], C]``) and would make it impossible for constructors to | ||||||
| be passthrough (``T(5) == C(5)`` when ``C = Annotation[T, Ann]``). | ||||||
|
|
||||||
| This feature was left out to keep the design simple: | ||||||
|
|
||||||
| * ``Annotated`` cannot be called with a single argument. Annotated could support | ||||||
| returning the underlying value when called with a single argument (e.g.: | ||||||
| ``Annotated[int] == int``). This complicates the specifications and adds | ||||||
| little benefit. | ||||||
|
|
||||||
|
|
||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add a short section about rejected ideas (even if there is currently just few of them, since more can appear during the discussion). |
||||||
| References | ||||||
| ---------- | ||||||
|
|
||||||
| .. [issue-600] | ||||||
| https://github.com/python/typing/issues/600 | ||||||
|
|
||||||
| .. [python-ideas] | ||||||
| https://mail.python.org/pipermail/python-ideas/2019-January/054908.html | ||||||
|
|
||||||
| .. [struct-doc] | ||||||
| https://docs.python.org/3/library/struct.html#examples | ||||||
|
|
||||||
| .. [mypy] | ||||||
| http://www.mypy-lang.org/ | ||||||
|
|
||||||
| .. [pyre] | ||||||
| https://pyre-check.org/ | ||||||
|
|
||||||
| .. [pycharm] | ||||||
| https://www.jetbrains.com/pycharm/ | ||||||
|
|
||||||
| .. [pytype] | ||||||
| https://github.com/google/pytype | ||||||
|
|
||||||
| .. [mypy-plugin] | ||||||
| https://github.com/python/mypy_extensions | ||||||
|
|
||||||
| .. [tagged-union] | ||||||
| https://en.wikipedia.org/wiki/Tagged_union | ||||||
|
|
||||||
| .. [typed-dict] | ||||||
| https://mypy.readthedocs.io/en/latest/more_types.html#typeddict | ||||||
|
|
||||||
| .. [issue-276] | ||||||
| https://github.com/python/typing/issues/276 | ||||||
|
|
||||||
| Copyright | ||||||
| --------- | ||||||
|
|
||||||
| This document has been placed in the public domain. | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I am thinking about a possible better title for this PEP. What do you think about "Flexible function and variable annotations"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Mixing typing and non-typing annotations"? "Extra [meta]data in type hints"? "Non-typing information in type hints?" Just spitballing here.