PEP 563: Postponed Evaluation of Annotations, first draft

PEP: 563 Title: Postponed Evaluation of Annotations Version: $Revision$ Last-Modified: $Date$ Author: Łukasz Langa <lukasz@langa.pl> Discussions-To: Python-Dev <python-dev@python.org> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 8-Sep-2017 Python-Version: 3.7 Post-History: Resolution: Abstract ======== PEP 3107 introduced syntax for function annotations, but the semantics were deliberately left undefined. PEP 484 introduced a standard meaning to annotations: type hints. PEP 526 defined variable annotations, explicitly tying them with the type hinting use case. This PEP proposes changing function annotations and variable annotations so that they are no longer evaluated at function definition time. Instead, they are preserved in ``__annotations__`` in string form. This change is going to be introduced gradually, starting with a new ``__future__`` import in Python 3.7. Rationale and Goals =================== PEP 3107 added support for arbitrary annotations on parts of a function definition. Just like default values, annotations are evaluated at function definition time. This creates a number of issues for the type hinting use case: * forward references: when a type hint contains names that have not been defined yet, that definition needs to be expressed as a string literal; * type hints are executed at module import time, which is not computationally free. Postponing the evaluation of annotations solves both problems. Non-goals --------- Just like in PEP 484 and PEP 526, it should be emphasized that **Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.** Annotations are still available for arbitrary use besides type checking. Using ``@typing.no_type_hints`` in this case is recommended to disambiguate the use case. Implementation ============== In a future version of Python, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective ``__annotations__`` dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation. If an annotation was already a string, this string is preserved verbatim. In other cases, the string form is obtained from the AST during the compilation step, which means that the string form preserved might not preserve the exact formatting of the source. Annotations need to be syntactically valid Python expressions, also when passed as literal strings (i.e. ``compile(literal, '', 'eval')``). Annotations can only use names present in the module scope as postponed evaluation using local names is not reliable. Note that as per PEP 526, local variable annotations are not evaluated at all since they are not accessible outside of the function's closure. Enabling the future behavior in Python 3.7 ------------------------------------------ The functionality described above can be enabled starting from Python 3.7 using the following special import:: from __future__ import annotations Resolving Type Hints at Runtime =============================== To resolve an annotation at runtime from its string form to the result of the enclosed expression, user code needs to evaluate the string. For code that uses type hints, the ``typing.get_type_hints()`` function correctly evaluates expressions back from its string form. Note that all valid code currently using ``__annotations__`` should already be doing that since a type annotation can be expressed as a string literal. For code which uses annotations for other purposes, a regular ``eval(ann, globals, locals)`` call is enough to resolve the annotation. The trick here is to get the correct value for globals. Fortunately, in the case of functions, they hold a reference to globals in an attribute called ``__globals__``. To get the correct module-level context to resolve class variables, use:: cls_globals = sys.modules[SomeClass.__module__].__dict__ Runtime annotation resolution and class decorators -------------------------------------------------- Metaclasses and class decorators that need to resolve annotations for the current class will fail for annotations that use the name of the current class. Example:: def class_decorator(cls): annotations = get_type_hints(cls) # raises NameError on 'C' print(f'Annotations for {cls}: {annotations}') return cls @class_decorator class C: singleton: 'C' = None This was already true before this PEP. The class decorator acts on the class before it's assigned a name in the current definition scope. The situation is made somewhat stricter when class-level variables are considered. Previously, when the string form wasn't used in annotations, a class decorator would be able to cover situations like:: @class_decorator class Restaurant: class MenuOption(Enum): SPAM = 1 EGGS = 2 default_menu: List[MenuOption] = [] This is no longer possible. Runtime annotation resolution and ``TYPE_CHECKING`` --------------------------------------------------- Sometimes there's code that must be seen by a type checker but should not be executed. For such situations the ``typing`` module defines a constant, ``TYPE_CHECKING``, that is considered ``True`` during type checking but ``False`` at runtime. Example:: import typing if typing.TYPE_CHECKING: import expensive_mod def a_func(arg: expensive_mod.SomeClass) -> None: a_var: expensive_mod.SomeClass = arg ... This approach is also useful when handling import cycles. Trying to resolve annotations of ``a_func`` at runtime using ``typing.get_type_hints()`` will fail since the name ``expensive_mod`` is not defined (``TYPE_CHECKING`` variable being ``False`` at runtime). This was already true before this PEP. Backwards Compatibility ======================= This is a backwards incompatible change. Applications depending on arbitrary objects to be directly present in annotations will break if they are not using ``typing.get_type_hints()`` or ``eval()``. Annotations that depend on locals at the time of the function/class definition are now invalid. Example:: def generate_class(): some_local = datetime.datetime.now() class C: field: some_local = 1 # NOTE: INVALID ANNOTATION def method(self, arg: some_local.day) -> None: # NOTE: INVALID ANNOTATION ... Annotations using nested classes and their respective state are still valid, provided they use the fully qualified name. Example:: class C: field = 'c_field' def method(self, arg: C.field) -> None: # this is OK ... class D: field2 = 'd_field' def method(self, arg: C.field -> C.D.field2: # this is OK ... In the presence of an annotation that cannot be resolved using the current module's globals, a NameError is raised at compile time. Deprecation policy ------------------ In Python 3.7, a ``__future__`` import is required to use the described functionality and a ``PendingDeprecationWarning`` is raised by the compiler in the presence of type annotations in modules without the ``__future__`` import. In Python 3.8 the warning becomes a ``DeprecationWarning``. In the next version this will become the default behavior. Rejected Ideas ============== Keep the ability to use local state when defining annotations ------------------------------------------------------------- With postponed evaluation, this is impossible for function locals. For classes, it would be possible to keep the ability to define annotations using the local scope. However, when using ``eval()`` to perform the postponed evaluation, we need to provide the correct globals and locals to the ``eval()`` call. In the face of nested classes, the routine to get the effective "globals" at definition time would have to look something like this:: def get_class_globals(cls): result = {} result.update(sys.modules[cls.__module__].__dict__) for child in cls.__qualname__.split('.'): result.update(result[child].__dict__) return result This is brittle and doesn't even cover slots. Requiring the use of module-level names simplifies runtime evaluation and provides the "one obvious way" to read annotations. It's the equivalent of absolute imports. Acknowledgements ================ This document could not be completed without valuable input, encouragement and advice from Guido van Rossum, Jukka Lehtosalo, and Ivan Levkivskyi. Copyright ========= This document has been placed in the public domain. .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End:

I like it. For previous discussion of this idea see here: https://mail.python.org/pipermail/python-ideas/2016-September/042527.html I don't see this mentioned in the PEP, but it will also allow (easy) description of contracts and dependent types. Elazar On Mon, Sep 11, 2017 at 6:59 PM Lukasz Langa <lukasz@langa.pl> wrote:

On Mon, Sep 11, 2017 at 04:06:14PM +0000, אלעזר wrote:
How? You may be using a different meaning to the word "contract" than I'm familiar with. I'm thinking about Design By Contract, where the contracts are typically much more powerful than mere type checks, e.g. a contract might state that the argument is float between 0 and 1, or that the return result is datetime object in the future. There are (at least?) three types of contracts: preconditions, which specify the arguments, postconditions, which specify the return result, and invariants, which specify what doesn't change. I don't see how you can specify contracts in a single type annotation. -- Steve

On Mon, Sep 11, 2017 at 8:58 PM Steven D'Aprano <steve@pearwood.info> wrote:
familiar with. I'm thinking about Design By Contract, where the
contracts are typically much more powerful than mere type checks, e.g. a contract might state that the argument is float between 0 and 1,
def f(x: float and (0 <= x <= 1)) -> float: ...
Exemplified above
postconditions, which specify the return result,
def f(x: int, y: int) -> ret < y: # ret being an (ugly) convention, unknown to python ...
and invariants, which specify what doesn't change.
class A: x: x != 0 y: y > x def foo(self): ... Of course I'm not claiming my specific examples are useful or readable. I'm also not claiming anything about the ability to check or enforce it; it's merely about making python more friendly to 3rd party tool support. You didn't ask about dependent types, but an example is in order: def zip(*x: Tuple[List[T], _n]) -> List[Tuple[T, _n]]: ... # _n being a binding occurrence, again not something the interpreter should know Basically dependent types, like other types, are just a restricted form of contracts. Elazar

This is off topic for discussion of this PEP. It would require another one (essentially an extension of PEP 484) to get passed for your idea to be standardized. For now, I don't want to distract reviewers from conflating PEP 563 with all the possible wonderful or horrible ways people can potentially extend type hints with. The biggest achievement of PEP 484 is creating a _standard_ syntax for typing in Python that other tools can embrace. I want to be very explicit that in no way should PEP 563 be viewed as a gateway to custom extensions that are going to go against the agreed standard. Further evolution of PEP 484 is possible (as exemplified by PEP 526) but even though PEP 563 does create several opportunities for nicer syntax, this is off topic for the time being. - Ł

On Mon, Sep 11, 2017 at 10:07 PM Lukasz Langa <lukasz@langa.pl> wrote:
I'm not sure whether this is directed to me; so just to make it clear, I did not propose anything here. I will be happy for PEP 563 to be accepted as is, although the implications of the ability to customize Python's scoping/binding rules are worth mentioning in the PEP, regardless of whether these are considered a pro or a con. Elazar

One thing I want to point out: there are a lot of really useful Python libraries that have come to rely on annotations being objects, ranging from plac to fbuild to many others. I could understand something that delays the evaluation of annotations until they are accessed, but this seems really extreme. On Mon, Sep 11, 2017 at 10:58 AM, Lukasz Langa <lukasz@langa.pl> wrote:
-- Ryan (ライアン) Yoko Shimomura, ryo (supercell/EGOIST), Hiroyuki Sawano >> everyone else http://refi64.com/

Ryan Gonzalez schrieb am 11.09.2017 um 19:16:
I guess there could be some helper that would allow you to say "here's an annotation or a function, here's the corresponding module globals(), please give me the annotation instances". But the "__annotations__" mapping could probably also be self-expanding on first request, if it remembers its globals(). Stefan

Currently the PEP simply proposes using eval(ann, globals, locals) and even suggests where to take globals from. The problem is with nested classes or type annotations that are using local state. The PEP is proposing to disallow those due to the trickiness of getting the global and local state right in those situations. Instead, you'd use qualified names for class-level fields that you're using in your annotation. This change is fine for static use and runtime use, except when faced with metaclasses or class decorators which resolve the annotations of a class in question. So far it looks like both typing.NamedTuple and the proposed data classes are fine with this. But if you have examples of metaclasses or class decorators which would break, let me know! - Ł

Shout out to fbuild which is a project that was built on Python 3 since early 2008, running on RCs of Python 3.0! Mind=blown. We are aware of fbuild, plac, and dryparse, Larry Hastings' similar project. But I definitely wouldn't say there are "a lot of" libraries like that, in fact I only know of a handful more, most of them early stage "runtime type checkers" with minimal adoption. When PEP 484 was put for review, we were testing the waters here, and added wording like: "In order for maximal compatibility with offline type checking it may eventually be a good idea to change interfaces that rely on annotations to switch to a different mechanism, for example a decorator." and "We do hope that type hints will eventually become the sole use for annotations, but this will require additional discussion and a deprecation period after the initial roll-out of the typing module. (...) Another possible outcome would be that type hints will eventually become the default meaning for annotations, but that there will always remain an option to disable them." Turns out, only authors of a few libraries spoke up AFAICT and most were happy with @no_type_hints. I remember mostly Stefan Behnel's concerns about Cython's annotations, and those never really took off. The bigger uproar at the time was against Python becoming a "statically typed language". Summing up, PEP 563 is proposing a backwards incompatible change but it doesn't look like it's going to affect "a lot of" libraries. More importantly, it does provide a way for those libraries to keep working.
This PEP is proposing delaying evaluation until annotations are accessed but gives user code the power to decide whether the string form is enough, or maybe an AST would be enough, or actual evaluation with get_type_hints() or eval() is necessary. - Ł

On Mon, Sep 11, 2017 at 3:25 PM, Lukasz Langa <lukasz@langa.pl> wrote: [..]
I'm one of those who used annotations for other purposes than type hints. And even if annotations became strings in Python 3.7 *without future import*, fixing my libraries would be easy -- just add an eval(). That said, the PEP doesn't cover an alternative solution: 1. Add another special attribute to functions: __annotations_text__. 2. __annotations__ becomes a dynamic Mapping, which evaluates stuff from __annotations_text__ *lazily*. 3. Recommend linters and IDEs to support "# pragma: annotations", as a way to say that the Python files follows the new Python 3.7 annotations semantics. That would maintain full backwards compatibility with all existing Python libraries and would not require a future import. Yury

I'm not very thrilled about this because lazy evaluation is subject to the new scoping rules (can't use local state) and might give a different result than before. It's not backwards compatible. A __future__ import makes it obvious that behavior is going to be different. And lazy evaluation is an unnecessary step if `get_type_hints()` is used later on so it's unnecessary for the most common usage of annotations. Finally, I don't think we ever had a "# pragma" suggestion coming from CPython. In reality, people wouldn't bother putting it in most files so tools would have to assume that forward references are correct *and* avoid raising errors about invalid names used in annotations. This is loss of functionality. - Ł

Lukasz Langa schrieb am 11.09.2017 um 21:25:
I remember mostly Stefan Behnel's concerns about Cython's annotations,
I'm currently reimplementing the annotation typing in Cython to be compatible with PEP-484, so that concern is pretty much out of the way. This PEP still has an impact on Cython, because we'd have to implement the same thing, and also make the interface available in older Python versions (2.6+) for Cython compiled modules. Stefan

In principle, I like this idea, this will save some keystrokes and will make annotated code more "beautiful". But I am quite worried about the backwards compatibility. One possible idea would be to use __future__ import without a definite deprecation plan. If people will be fine with using typing.get_type_hints (btw this is already the preferred way instead of directly accessing __annotations__, according to PEP 526 at least) then we could go ahead with deprecation. Also I really like Yury's idea of dynamic mapping, but it has one downside, semantics of this will change: def fun(x: print("Function defined"), y: int) -> None: ... However I agree functions with side effects in annotations are very rare, and it would be reasonable to sacrifice this tiny backwards compatibility to avoid the __future__ import. -- Ivan

This is not a viable strategy since __future__ is not designed to be a feature toggle but rather to be a gradual introduction of an upcoming breaking change.
As you're pointing out, people already have to use `typing.get_type_hints()`, otherwise they are already failing evaluation of existing forward references. Accessing __annotations__ directly in this context is a bug today.
Also I really like Yury's idea of dynamic mapping
I responded to his idea under his post. - Ł

On Mon, Sep 11, 2017 at 10:16 AM, Ryan Gonzalez <rymg19@gmail.com> wrote:
This is a serious concern and we need to give it some thought. The current thinking is that those libraries can still get those objects by simply applying eval() to the annotations (or typing.get_type_hints()). And they may already have to support that in order to support PEP 484's forward references. Though perhaps there's a reason why such libraries currently don't need to handle forward refs? -- --Guido van Rossum (python.org/~guido)

On Mon, Sep 11, 2017 at 11:58:45AM -0400, Lukasz Langa wrote:
PEP: 563 Title: Postponed Evaluation of Annotations
A few comments, following the quoted passages as needed.
You haven't justified that these are problems large enough to need fixing, let alone fixing in a backwards-incompatible way. Regarding forward references: I see no problem with quoting forward references. Some people complain about the quotation marks, but frankly I don't think that's a major imposition. I'm not sure that going from a situation where only forward references are strings, to one where all annotations are strings, counts as a positive solution. Regarding the execution time at runtime: this sounds like premature optimization. If it is not, if you have profiled some applications found that there is more than a trivial amount of time used by generating the annotations, you should say so. In my opinion, a less disruptive solution to the execution time (supposed?) problem is a switch to disable annotations altogether, similar to the existing -O optimize switch, turning them into no-ops. That won't make any difference to static type-checkers. Some people will probably want such a switch anyway, if they care about the memory used by the __annotations__ dictionaries.
Can you give an example of how the AST may distort the source? My knee-jerk reaction is that anything which causes the annotation to differ from the source is likely to cause confusion.
And that's a problem. Forcing the use of global variables seems harmful. Preventing the use of locals seems awful. Can't we use some sort of closure-like mechanism? This restriction is going to break, or prevent, situations like this: def decorator(func): kind = ... # something generated at decorator call time @functools.wraps(func) def inner(arg: kind): ... return inner Even if static typecheckers have no clue what the annotation on the inner function is, it is still useful for introspection and documentation.
A small style issue: I think that's better written as: cls_globals = vars(sys.modules[SomeClass.__module__]) We should avoid directly accessing dunders unless necessary, and vars() exists specifically for the purpose of returning object's __dict__.
I don't know whether this is important, but for the record the current documentation shows expensive_mod.SomeClass quoted.
As mentioned above, I think this is a bad loss of existing functionality. [...]
In the presence of an annotation that cannot be resolved using the current module's globals, a NameError is raised at compile time.
It is not clear what this means, or how it will work, especially given that the point of this is to delay evaluating annotations. How will the compiler know that an annotation cannot be resolved if it doesn't try to evaluate it? Which brings me to another objection. In general, errors should be caught as early as possible. Currently, many (but not all) errors in annotations are caught at runtime because their evaluation fails: class MyClass: ... def function(arg: MyClsas) -> int: # oops ... That's a nice feature even if I'm not using a type-checker: I get immediate feedback as soon as I try running the code that my annotation is wrong. It is true that forward references aren't evaluated at runtime, because they are strings, but "ordinary" annotations are evaluated, and that's a good thing! Losing that seems like a step backwards.
Impossible seems a bit strong. Can you elaborate? [...]
I hardly think that "simplifies runtime evaluation" is true. At the moment annotations are already evaluated. *Anything* that you have to do by hand (like call eval) cannot be simpler than "do nothing". I don't think the analogy with absolute imports is even close to useful, and far from being "one obvious way", this is a surprising, non-obvious, seemingly-arbitrary restriction on annotations. Given that for *six versions* of Python 3 annotations could be locals, there is nothing obvious about restricting them to globals. -- Steve

On 12 September 2017 at 11:45, Steven D'Aprano <steve@pearwood.info> wrote:
I actually agree with this, and I think there's an alternative to string evaluation that would solve the "figure out the right globals() & locals() references" problem in a more elegant way: instead of using strings, implicitly compile the annotations as "lambda: <expr>". You'd still lose the ability to access class locals (except by their qualified name), but you'd be able to access function locals just fine, since the lambda expression would implicitly generate the necessary closure cells to keep the relevant local variables alive following the termination of the outer function. Unfortunately, this idea has the downside that for trivial annotations, defining a lambda expression is likely to be *slower* than evaluating the expression, whereas referencing a string constant is faster: $ python -m perf timeit "int" ..................... Mean +- std dev: 27.7 ns +- 1.8 ns $ python -m perf timeit "lambda: int" ..................... Mean +- std dev: 66.0 ns +- 1.7 ns $ python -m perf timeit "'int'" ..................... Mean +- std dev: 7.97 ns +- 0.32 ns Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Tue, Sep 12, 2017 at 09:17:23PM +1000, Nick Coghlan wrote:
Is it time to consider a specialised, high-speed (otherwise there's no point) thunk that can implicitly capture the environment like a function, but has less overhead? For starters, you don't need to care about argument passing. -- Steve

Thanks for your detailed review!
Are you a user of type annotations? In the introduction of typing at Facebook this is the single most annoying thing people point out. The reason is that it's not obvious and the workaround is ugly. Learning about this quirk adds to an already significant body of knowledge new typing users have to familiarize themselves with. It's also aesthetically jarring, the original PEP 484 admits this is a clunky solution.
I used hand-wavy language because I didn't really check before. This time around I'm coming back prepared. Instagram has roughly 10k functions annotated at this point. Using tracemalloc I tested how much memory it takes to import modules with those functions. Then I compared how much memory it takes to import modules with those functions when annotations are stripped from the entire module (incl. module-level variable annotations, aliases, class variable annotations, etc.). The difference in allocated memory is over 22 MB. The import time with annotations is over 2s longer. The problem with those numbers that we still have 80% functions to cover.
Yes, this is a possibility that I should address in the PEP explicitly. There are two reasons this is not satisfying: 1. This only addresses runtime cost, not forward references, those still cannot be safely used in source code. Even if a single one was used in a single module, the entire program now has to be executed with this new hypothetical -O switch. Nobody would agree to dependencies like that. 2. This throws the baby out with the bath water. Now *no* runtime annotation use can be performed. There's work on new tools that use PEP 484-compliant type annotations at runtime (Larry is reviving his dryparse library, Eric Smith is working on data classes). Those would not work with this hypothetical -O switch but are fine with the string form since they already need to use `typing.get_type_hints()`.
Anything not explicitly stored in the AST will require a normalized untokenization. This covers whitespace, punctuation, and brackets. An example: def fun(a: Dict[ str, str, ]) -> None: ... would become something like: {'a': 'Dict[(str, str)]', 'return': 'None'}
As Nick measured, currently closures would add to the performance problem, not solve it. What is an actual use case that this would prevent?
Do you have a specific use case in mind? If you need to put magic dynamic annotations for runtime introspection, the decorator can apply those directly via calling inner.__annotations__ = {'arg': 'kind'} This is what the "attrs" library is considering doing for the magic __init__ method. Having annotation literals in the source not validated would not hinder that at all. That's it for introspection. As for documentation, I don't really understand that comment.
Right! Would be great if it composed a dictionary of slots for us, too ;-)
Yes, I wanted to explicitly illustrate that now you can use the annotations directly without quoting and it won't fail at import time. If you tried to evaluate them with `typing.get_type_hints()` that would fail, just like trying to evaluate the string form today.
I don't quite see it. If somebody badly needs to store arbitrary data as annotations in generated functions, they still can directly write to `__annotations__`. In every other case, specifically in static typing (the overwhelming use case for annotations), this makes human readers happier and static analysis tools are indifferent. In fact, if runtime type hint evaluation is necessary, then even if a library is already using `typing.get_type_hints()` (which it should!), so far I haven't seen much use of `globalns` and `localns` arguments to this function which suggests that the forward references we can support are already constrained to globals anyway.
The idea would be to validate the expressions after the entire module is compiled, something like what the flake8-pyi plugin is doing today for .pyi files. Guido pointed out that it's not trivial since the compiler doesn't keep a symbol table around. But I'd invest time in this since I agree with your point that we should raise errors as early as possible.
Unless we stored the closure, it's not possible.
This section covers the rejected idea of allowing local access on classes specifically. As I mentioned earlier, preserving local function scope access is not something we can do without bending ourselves backwards. Constraining ourselves to only global scope simplifies runtime evaluation compared to that use case. The alternative is not "do nothing".
That's a bit harsh. I probably didn't voice my idea clearly enough here so let me elaborate. Annotations inside nested classes which are using local scope currently have to use the local names directly instead of using the qualified name. This has similar issues to relative imports: class Menu(UIComponent): ... class Restaurant: class Menu(Enum): SPAM = 1 EGGS = 2 def generate_menu_component(self) -> Menu: ... It is impossible today to use the global "Menu" type in the annotation of the example method. This PEP is proposing to use qualified names in this case which disambiguates between the global "Menu" and "Restaurant.Menu". In this sense it felt similar to absolute imports to me.
Given that for *six versions* of Python 3 annotations could be locals, there is nothing obvious about restricting them to globals.
I don't understand the argument of status quo. In those six versions, the first two were not usable in production and the following two only slowly started getting adoption. It wasn't until Python 3.5 that we started seeing significant adoption figures. This is incidentally also the first release with PEP 484. Wow, this took quite some time to respond to, sorry it took so long! I had to do additional research and clarify some of my own understanding while doing this. It was very valuable, thanks again for your feedback! - Ł

This will not be a problem with PEP 560 (I could imagine that string objects may take actually more memory than relatively small cached objects). Also I think it makes sense to mention in the PEP that stringifying annotations does not solve _all_ problems with forward references. For example, two typical situations are: T = TypeVar('T', bound='Factory') class Factory: def make_copy(self: T) -> T: ... and class Vertex(List['Edge']): ... class Edge: ends: Tuple[Vertex, Vertex] Actually both situations can be resolved with PEP 563 if one puts `T` after `Factory`, and `Vertex` after `Edge`, the latter is OK, but the former would be strange. After all, it is OK to pay a _little_ price for Python being an interpreted language. There are other situations discussed in https://github.com/python/typing/issues/400, I don't want to copy all of them to the PEP, but I think this prior discussion should be referenced in the PEP.
But how it was with `from __future__ import division`? What I was proposing is something similar, just have `from __future__ import annotations` that will be default in Python 4. (Although this time it would be a good idea to emit DeprecationWarning one-two releases before Python 4). -- Ivan

I like it. For previous discussion of this idea see here: https://mail.python.org/pipermail/python-ideas/2016-September/042527.html I don't see this mentioned in the PEP, but it will also allow (easy) description of contracts and dependent types. Elazar On Mon, Sep 11, 2017 at 6:59 PM Lukasz Langa <lukasz@langa.pl> wrote:

On Mon, Sep 11, 2017 at 04:06:14PM +0000, אלעזר wrote:
How? You may be using a different meaning to the word "contract" than I'm familiar with. I'm thinking about Design By Contract, where the contracts are typically much more powerful than mere type checks, e.g. a contract might state that the argument is float between 0 and 1, or that the return result is datetime object in the future. There are (at least?) three types of contracts: preconditions, which specify the arguments, postconditions, which specify the return result, and invariants, which specify what doesn't change. I don't see how you can specify contracts in a single type annotation. -- Steve

On Mon, Sep 11, 2017 at 8:58 PM Steven D'Aprano <steve@pearwood.info> wrote:
familiar with. I'm thinking about Design By Contract, where the
contracts are typically much more powerful than mere type checks, e.g. a contract might state that the argument is float between 0 and 1,
def f(x: float and (0 <= x <= 1)) -> float: ...
Exemplified above
postconditions, which specify the return result,
def f(x: int, y: int) -> ret < y: # ret being an (ugly) convention, unknown to python ...
and invariants, which specify what doesn't change.
class A: x: x != 0 y: y > x def foo(self): ... Of course I'm not claiming my specific examples are useful or readable. I'm also not claiming anything about the ability to check or enforce it; it's merely about making python more friendly to 3rd party tool support. You didn't ask about dependent types, but an example is in order: def zip(*x: Tuple[List[T], _n]) -> List[Tuple[T, _n]]: ... # _n being a binding occurrence, again not something the interpreter should know Basically dependent types, like other types, are just a restricted form of contracts. Elazar

This is off topic for discussion of this PEP. It would require another one (essentially an extension of PEP 484) to get passed for your idea to be standardized. For now, I don't want to distract reviewers from conflating PEP 563 with all the possible wonderful or horrible ways people can potentially extend type hints with. The biggest achievement of PEP 484 is creating a _standard_ syntax for typing in Python that other tools can embrace. I want to be very explicit that in no way should PEP 563 be viewed as a gateway to custom extensions that are going to go against the agreed standard. Further evolution of PEP 484 is possible (as exemplified by PEP 526) but even though PEP 563 does create several opportunities for nicer syntax, this is off topic for the time being. - Ł

On Mon, Sep 11, 2017 at 10:07 PM Lukasz Langa <lukasz@langa.pl> wrote:
I'm not sure whether this is directed to me; so just to make it clear, I did not propose anything here. I will be happy for PEP 563 to be accepted as is, although the implications of the ability to customize Python's scoping/binding rules are worth mentioning in the PEP, regardless of whether these are considered a pro or a con. Elazar

One thing I want to point out: there are a lot of really useful Python libraries that have come to rely on annotations being objects, ranging from plac to fbuild to many others. I could understand something that delays the evaluation of annotations until they are accessed, but this seems really extreme. On Mon, Sep 11, 2017 at 10:58 AM, Lukasz Langa <lukasz@langa.pl> wrote:
-- Ryan (ライアン) Yoko Shimomura, ryo (supercell/EGOIST), Hiroyuki Sawano >> everyone else http://refi64.com/

Ryan Gonzalez schrieb am 11.09.2017 um 19:16:
I guess there could be some helper that would allow you to say "here's an annotation or a function, here's the corresponding module globals(), please give me the annotation instances". But the "__annotations__" mapping could probably also be self-expanding on first request, if it remembers its globals(). Stefan

Currently the PEP simply proposes using eval(ann, globals, locals) and even suggests where to take globals from. The problem is with nested classes or type annotations that are using local state. The PEP is proposing to disallow those due to the trickiness of getting the global and local state right in those situations. Instead, you'd use qualified names for class-level fields that you're using in your annotation. This change is fine for static use and runtime use, except when faced with metaclasses or class decorators which resolve the annotations of a class in question. So far it looks like both typing.NamedTuple and the proposed data classes are fine with this. But if you have examples of metaclasses or class decorators which would break, let me know! - Ł

Shout out to fbuild which is a project that was built on Python 3 since early 2008, running on RCs of Python 3.0! Mind=blown. We are aware of fbuild, plac, and dryparse, Larry Hastings' similar project. But I definitely wouldn't say there are "a lot of" libraries like that, in fact I only know of a handful more, most of them early stage "runtime type checkers" with minimal adoption. When PEP 484 was put for review, we were testing the waters here, and added wording like: "In order for maximal compatibility with offline type checking it may eventually be a good idea to change interfaces that rely on annotations to switch to a different mechanism, for example a decorator." and "We do hope that type hints will eventually become the sole use for annotations, but this will require additional discussion and a deprecation period after the initial roll-out of the typing module. (...) Another possible outcome would be that type hints will eventually become the default meaning for annotations, but that there will always remain an option to disable them." Turns out, only authors of a few libraries spoke up AFAICT and most were happy with @no_type_hints. I remember mostly Stefan Behnel's concerns about Cython's annotations, and those never really took off. The bigger uproar at the time was against Python becoming a "statically typed language". Summing up, PEP 563 is proposing a backwards incompatible change but it doesn't look like it's going to affect "a lot of" libraries. More importantly, it does provide a way for those libraries to keep working.
This PEP is proposing delaying evaluation until annotations are accessed but gives user code the power to decide whether the string form is enough, or maybe an AST would be enough, or actual evaluation with get_type_hints() or eval() is necessary. - Ł

On Mon, Sep 11, 2017 at 3:25 PM, Lukasz Langa <lukasz@langa.pl> wrote: [..]
I'm one of those who used annotations for other purposes than type hints. And even if annotations became strings in Python 3.7 *without future import*, fixing my libraries would be easy -- just add an eval(). That said, the PEP doesn't cover an alternative solution: 1. Add another special attribute to functions: __annotations_text__. 2. __annotations__ becomes a dynamic Mapping, which evaluates stuff from __annotations_text__ *lazily*. 3. Recommend linters and IDEs to support "# pragma: annotations", as a way to say that the Python files follows the new Python 3.7 annotations semantics. That would maintain full backwards compatibility with all existing Python libraries and would not require a future import. Yury

I'm not very thrilled about this because lazy evaluation is subject to the new scoping rules (can't use local state) and might give a different result than before. It's not backwards compatible. A __future__ import makes it obvious that behavior is going to be different. And lazy evaluation is an unnecessary step if `get_type_hints()` is used later on so it's unnecessary for the most common usage of annotations. Finally, I don't think we ever had a "# pragma" suggestion coming from CPython. In reality, people wouldn't bother putting it in most files so tools would have to assume that forward references are correct *and* avoid raising errors about invalid names used in annotations. This is loss of functionality. - Ł

Lukasz Langa schrieb am 11.09.2017 um 21:25:
I remember mostly Stefan Behnel's concerns about Cython's annotations,
I'm currently reimplementing the annotation typing in Cython to be compatible with PEP-484, so that concern is pretty much out of the way. This PEP still has an impact on Cython, because we'd have to implement the same thing, and also make the interface available in older Python versions (2.6+) for Cython compiled modules. Stefan

In principle, I like this idea, this will save some keystrokes and will make annotated code more "beautiful". But I am quite worried about the backwards compatibility. One possible idea would be to use __future__ import without a definite deprecation plan. If people will be fine with using typing.get_type_hints (btw this is already the preferred way instead of directly accessing __annotations__, according to PEP 526 at least) then we could go ahead with deprecation. Also I really like Yury's idea of dynamic mapping, but it has one downside, semantics of this will change: def fun(x: print("Function defined"), y: int) -> None: ... However I agree functions with side effects in annotations are very rare, and it would be reasonable to sacrifice this tiny backwards compatibility to avoid the __future__ import. -- Ivan

This is not a viable strategy since __future__ is not designed to be a feature toggle but rather to be a gradual introduction of an upcoming breaking change.
As you're pointing out, people already have to use `typing.get_type_hints()`, otherwise they are already failing evaluation of existing forward references. Accessing __annotations__ directly in this context is a bug today.
Also I really like Yury's idea of dynamic mapping
I responded to his idea under his post. - Ł

On Mon, Sep 11, 2017 at 10:16 AM, Ryan Gonzalez <rymg19@gmail.com> wrote:
This is a serious concern and we need to give it some thought. The current thinking is that those libraries can still get those objects by simply applying eval() to the annotations (or typing.get_type_hints()). And they may already have to support that in order to support PEP 484's forward references. Though perhaps there's a reason why such libraries currently don't need to handle forward refs? -- --Guido van Rossum (python.org/~guido)

On Mon, Sep 11, 2017 at 11:58:45AM -0400, Lukasz Langa wrote:
PEP: 563 Title: Postponed Evaluation of Annotations
A few comments, following the quoted passages as needed.
You haven't justified that these are problems large enough to need fixing, let alone fixing in a backwards-incompatible way. Regarding forward references: I see no problem with quoting forward references. Some people complain about the quotation marks, but frankly I don't think that's a major imposition. I'm not sure that going from a situation where only forward references are strings, to one where all annotations are strings, counts as a positive solution. Regarding the execution time at runtime: this sounds like premature optimization. If it is not, if you have profiled some applications found that there is more than a trivial amount of time used by generating the annotations, you should say so. In my opinion, a less disruptive solution to the execution time (supposed?) problem is a switch to disable annotations altogether, similar to the existing -O optimize switch, turning them into no-ops. That won't make any difference to static type-checkers. Some people will probably want such a switch anyway, if they care about the memory used by the __annotations__ dictionaries.
Can you give an example of how the AST may distort the source? My knee-jerk reaction is that anything which causes the annotation to differ from the source is likely to cause confusion.
And that's a problem. Forcing the use of global variables seems harmful. Preventing the use of locals seems awful. Can't we use some sort of closure-like mechanism? This restriction is going to break, or prevent, situations like this: def decorator(func): kind = ... # something generated at decorator call time @functools.wraps(func) def inner(arg: kind): ... return inner Even if static typecheckers have no clue what the annotation on the inner function is, it is still useful for introspection and documentation.
A small style issue: I think that's better written as: cls_globals = vars(sys.modules[SomeClass.__module__]) We should avoid directly accessing dunders unless necessary, and vars() exists specifically for the purpose of returning object's __dict__.
I don't know whether this is important, but for the record the current documentation shows expensive_mod.SomeClass quoted.
As mentioned above, I think this is a bad loss of existing functionality. [...]
In the presence of an annotation that cannot be resolved using the current module's globals, a NameError is raised at compile time.
It is not clear what this means, or how it will work, especially given that the point of this is to delay evaluating annotations. How will the compiler know that an annotation cannot be resolved if it doesn't try to evaluate it? Which brings me to another objection. In general, errors should be caught as early as possible. Currently, many (but not all) errors in annotations are caught at runtime because their evaluation fails: class MyClass: ... def function(arg: MyClsas) -> int: # oops ... That's a nice feature even if I'm not using a type-checker: I get immediate feedback as soon as I try running the code that my annotation is wrong. It is true that forward references aren't evaluated at runtime, because they are strings, but "ordinary" annotations are evaluated, and that's a good thing! Losing that seems like a step backwards.
Impossible seems a bit strong. Can you elaborate? [...]
I hardly think that "simplifies runtime evaluation" is true. At the moment annotations are already evaluated. *Anything* that you have to do by hand (like call eval) cannot be simpler than "do nothing". I don't think the analogy with absolute imports is even close to useful, and far from being "one obvious way", this is a surprising, non-obvious, seemingly-arbitrary restriction on annotations. Given that for *six versions* of Python 3 annotations could be locals, there is nothing obvious about restricting them to globals. -- Steve

On 12 September 2017 at 11:45, Steven D'Aprano <steve@pearwood.info> wrote:
I actually agree with this, and I think there's an alternative to string evaluation that would solve the "figure out the right globals() & locals() references" problem in a more elegant way: instead of using strings, implicitly compile the annotations as "lambda: <expr>". You'd still lose the ability to access class locals (except by their qualified name), but you'd be able to access function locals just fine, since the lambda expression would implicitly generate the necessary closure cells to keep the relevant local variables alive following the termination of the outer function. Unfortunately, this idea has the downside that for trivial annotations, defining a lambda expression is likely to be *slower* than evaluating the expression, whereas referencing a string constant is faster: $ python -m perf timeit "int" ..................... Mean +- std dev: 27.7 ns +- 1.8 ns $ python -m perf timeit "lambda: int" ..................... Mean +- std dev: 66.0 ns +- 1.7 ns $ python -m perf timeit "'int'" ..................... Mean +- std dev: 7.97 ns +- 0.32 ns Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Tue, Sep 12, 2017 at 09:17:23PM +1000, Nick Coghlan wrote:
Is it time to consider a specialised, high-speed (otherwise there's no point) thunk that can implicitly capture the environment like a function, but has less overhead? For starters, you don't need to care about argument passing. -- Steve

Thanks for your detailed review!
Are you a user of type annotations? In the introduction of typing at Facebook this is the single most annoying thing people point out. The reason is that it's not obvious and the workaround is ugly. Learning about this quirk adds to an already significant body of knowledge new typing users have to familiarize themselves with. It's also aesthetically jarring, the original PEP 484 admits this is a clunky solution.
I used hand-wavy language because I didn't really check before. This time around I'm coming back prepared. Instagram has roughly 10k functions annotated at this point. Using tracemalloc I tested how much memory it takes to import modules with those functions. Then I compared how much memory it takes to import modules with those functions when annotations are stripped from the entire module (incl. module-level variable annotations, aliases, class variable annotations, etc.). The difference in allocated memory is over 22 MB. The import time with annotations is over 2s longer. The problem with those numbers that we still have 80% functions to cover.
Yes, this is a possibility that I should address in the PEP explicitly. There are two reasons this is not satisfying: 1. This only addresses runtime cost, not forward references, those still cannot be safely used in source code. Even if a single one was used in a single module, the entire program now has to be executed with this new hypothetical -O switch. Nobody would agree to dependencies like that. 2. This throws the baby out with the bath water. Now *no* runtime annotation use can be performed. There's work on new tools that use PEP 484-compliant type annotations at runtime (Larry is reviving his dryparse library, Eric Smith is working on data classes). Those would not work with this hypothetical -O switch but are fine with the string form since they already need to use `typing.get_type_hints()`.
Anything not explicitly stored in the AST will require a normalized untokenization. This covers whitespace, punctuation, and brackets. An example: def fun(a: Dict[ str, str, ]) -> None: ... would become something like: {'a': 'Dict[(str, str)]', 'return': 'None'}
As Nick measured, currently closures would add to the performance problem, not solve it. What is an actual use case that this would prevent?
Do you have a specific use case in mind? If you need to put magic dynamic annotations for runtime introspection, the decorator can apply those directly via calling inner.__annotations__ = {'arg': 'kind'} This is what the "attrs" library is considering doing for the magic __init__ method. Having annotation literals in the source not validated would not hinder that at all. That's it for introspection. As for documentation, I don't really understand that comment.
Right! Would be great if it composed a dictionary of slots for us, too ;-)
Yes, I wanted to explicitly illustrate that now you can use the annotations directly without quoting and it won't fail at import time. If you tried to evaluate them with `typing.get_type_hints()` that would fail, just like trying to evaluate the string form today.
I don't quite see it. If somebody badly needs to store arbitrary data as annotations in generated functions, they still can directly write to `__annotations__`. In every other case, specifically in static typing (the overwhelming use case for annotations), this makes human readers happier and static analysis tools are indifferent. In fact, if runtime type hint evaluation is necessary, then even if a library is already using `typing.get_type_hints()` (which it should!), so far I haven't seen much use of `globalns` and `localns` arguments to this function which suggests that the forward references we can support are already constrained to globals anyway.
The idea would be to validate the expressions after the entire module is compiled, something like what the flake8-pyi plugin is doing today for .pyi files. Guido pointed out that it's not trivial since the compiler doesn't keep a symbol table around. But I'd invest time in this since I agree with your point that we should raise errors as early as possible.
Unless we stored the closure, it's not possible.
This section covers the rejected idea of allowing local access on classes specifically. As I mentioned earlier, preserving local function scope access is not something we can do without bending ourselves backwards. Constraining ourselves to only global scope simplifies runtime evaluation compared to that use case. The alternative is not "do nothing".
That's a bit harsh. I probably didn't voice my idea clearly enough here so let me elaborate. Annotations inside nested classes which are using local scope currently have to use the local names directly instead of using the qualified name. This has similar issues to relative imports: class Menu(UIComponent): ... class Restaurant: class Menu(Enum): SPAM = 1 EGGS = 2 def generate_menu_component(self) -> Menu: ... It is impossible today to use the global "Menu" type in the annotation of the example method. This PEP is proposing to use qualified names in this case which disambiguates between the global "Menu" and "Restaurant.Menu". In this sense it felt similar to absolute imports to me.
Given that for *six versions* of Python 3 annotations could be locals, there is nothing obvious about restricting them to globals.
I don't understand the argument of status quo. In those six versions, the first two were not usable in production and the following two only slowly started getting adoption. It wasn't until Python 3.5 that we started seeing significant adoption figures. This is incidentally also the first release with PEP 484. Wow, this took quite some time to respond to, sorry it took so long! I had to do additional research and clarify some of my own understanding while doing this. It was very valuable, thanks again for your feedback! - Ł

This will not be a problem with PEP 560 (I could imagine that string objects may take actually more memory than relatively small cached objects). Also I think it makes sense to mention in the PEP that stringifying annotations does not solve _all_ problems with forward references. For example, two typical situations are: T = TypeVar('T', bound='Factory') class Factory: def make_copy(self: T) -> T: ... and class Vertex(List['Edge']): ... class Edge: ends: Tuple[Vertex, Vertex] Actually both situations can be resolved with PEP 563 if one puts `T` after `Factory`, and `Vertex` after `Edge`, the latter is OK, but the former would be strange. After all, it is OK to pay a _little_ price for Python being an interpreted language. There are other situations discussed in https://github.com/python/typing/issues/400, I don't want to copy all of them to the PEP, but I think this prior discussion should be referenced in the PEP.
But how it was with `from __future__ import division`? What I was proposing is something similar, just have `from __future__ import annotations` that will be default in Python 4. (Although this time it would be a good idea to emit DeprecationWarning one-two releases before Python 4). -- Ivan
participants (10)
-
Ethan Smith
-
Guido van Rossum
-
Ivan Levkivskyi
-
Lukasz Langa
-
Nick Coghlan
-
Ryan Gonzalez
-
Stefan Behnel
-
Steven D'Aprano
-
Yury Selivanov
-
אלעזר