First public draft to be discussed on python-dev.

This commit is contained in:
Lukasz Langa 2017-11-01 14:30:38 -07:00
parent cbe3e2d540
commit adf528d4d0
1 changed files with 366 additions and 67 deletions

View File

@ -1,5 +1,4 @@
PEP: 563
Title: Postponed Evaluation of Annotations
PEP: 563 Title: Postponed Evaluation of Annotations
Version: $Revision$
Last-Modified: $Date$
Author: Łukasz Langa <lukasz@langa.pl>
@ -9,7 +8,7 @@ Type: Standards Track
Content-Type: text/x-rst
Created: 8-Sep-2017
Python-Version: 3.7
Post-History:
Post-History: 1-Nov-2017
Resolution:
@ -53,19 +52,40 @@ 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.
This PEP is meant to solve the problem of forward references in type
annotations. There are still cases outside of annotations where
forward references will require usage of string literals. Those are
listed in a later section of this document.
Annotations without forced evaluation enable opportunities to improve
the syntax of type hints. This idea will require its own separate PEP
and is not discussed further in this document.
Non-typing usage of annotations
-------------------------------
While annotations are still available for arbitrary use besides type
checking, it is worth mentioning that the design of this PEP, as well
as its precursors (PEP 484 and PEP 526), is predominantly motivated by
the type hinting use case.
With Python 3.7, PEP 484 graduates from provisional status. Other
enhancements to the Python programming language like PEP 544, PEP 557,
or PEP 560, are already being built on this basis as they depend on
type annotations and the ``typing`` module as defined by PEP 484.
With this in mind, uses for annotations incompatible with the
aforementioned PEPs should be considered deprecated.
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.
In Python 4.0, 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
@ -75,7 +95,8 @@ 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.
evaluation using local names is not reliable (with the sole exception of
class-level names resolved by ``typing.get_type_hints()``).
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.
@ -95,19 +116,51 @@ 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
For code that uses type hints, the
``typing.get_type_hints(obj, globalns=None, localns=None)`` 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::
annotation.
cls_globals = sys.modules[SomeClass.__module__].__dict__
In both cases it's important to consider how globals and locals affect
the postponed evaluation. An annotation is no longer evaluated at the
time of definition and, more importantly, *in the same scope* it was
defined. Consequently, using local state in annotations is no longer
possible in general. As for globals, the module where the annotation
was defined is the correct context for postponed evaluation.
The ``get_type_hints()`` function automatically resolves the correct
value of ``globalns`` for functions and classes. It also automatically
provides the correct ``localns`` for classes.
When running ``eval()``,
the value of globals can be gathered in the following way:
* function objects hold a reference to their respective globals in an
attribute called ``__globals__``;
* classes hold the name of the module they were defined in, this can be
used to retrieve the respective globals::
cls_globals = vars(sys.modules[SomeClass.__module__])
Note that this needs to be repeated for base classes to evaluate all
``__annotations__``.
* modules should use their own ``__dict__``.
The value of ``localns`` cannot be reliably retrieved for functions
because in all likelihood the stack frame at the time of the call no
longer exists.
For classes, ``localns`` can be composed by chaining vars of the given
class and its base classes (in the method resolution order). Since slots
can only be filled after the class was defined, we don't need to consult
them for this purpose.
Runtime annotation resolution and class decorators
--------------------------------------------------
@ -128,20 +181,6 @@ current class. Example::
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``
---------------------------------------------------
@ -174,18 +213,25 @@ 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::
Annotations that depend on locals at the time of the function
definition will not be resolvable later. Example::
def generate_class():
some_local = datetime.datetime.now()
def generate():
A = Optional[int]
class C:
field: some_local = 1 # NOTE: INVALID ANNOTATION
def method(self, arg: some_local.day) -> None: # NOTE: INVALID ANNOTATION
...
field: A = 1
def method(self, arg: A) -> None: ...
return C
X = generate()
Trying to resolve annotations of ``X`` later by using
``get_type_hints(X)`` will fail because ``A`` and its enclosing scope no
longer exists. Python will make no attempt to disallow such annotations
since they can often still be successfully statically analyzed, which is
the predominant use case for annotations.
Annotations using nested classes and their respective state are still
valid, provided they use the fully qualified name. Example::
valid. They can use local names or the fully qualified name. Example::
class C:
field = 'c_field'
@ -194,49 +240,302 @@ valid, provided they use the fully qualified name. Example::
class D:
field2 = 'd_field'
def method(self, arg: C.field -> C.D.field2: # this is OK
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.
def method(self, arg: field) -> D.field2: # this is OK
...
In the presence of an annotation that isn't a syntactically valid
expression, SyntaxError is raised at compile time. However, since names
aren't resolved at that time, no attempt is made to validate whether
used names are correct or not.
Deprecation policy
------------------
In Python 3.7, a ``__future__`` import is required to use the described
functionality and a ``PendingDeprecationWarning`` is raised by the
Starting with Python 3.7, a ``__future__`` import is required to use the
described functionality. No warnings are raised.
In Python 3.8 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.
``__future__`` import.
Starting with Python 3.9 the warning becomes a ``DeprecationWarning``.
In Python 4.0 this will become the default behavior. Use of annotations
incompatible with this PEP is no longer supported.
Forward References
==================
Deliberately using a name before it was defined in the module is called
a forward reference. For the purpose of this section, we'll call
any name imported or defined within a ``if TYPE_CHECKING:`` block
a forward reference, too.
This PEP addresses the issue of forward references in *type annotations*.
The use of string literals will no longer be required in this case.
However, there are APIs in the ``typing`` module that use other syntactic
constructs of the language, and those will still require working around
forward references with string literals. The list includes:
* type definitions::
T = TypeVar('T', bound='UserId')
UserId = NewType('UserId', 'SomeType')
Employee = NamedTuple('Employee', [('name', str), ('id', 'UserId')])
* aliases::
Alias = Optional['SomeType']
AnotherAlias = Union['SomeType', 'OtherType']
* casting::
cast('SomeType', value)
* base classes::
class C(Tuple['SomeType', 'OtherType']): ...
Depending on the specific case, some of the cases listed above might be
worked around by placing the usage in a ``if TYPE_CHECKING:`` block.
This will not work for any code that needs to be available at runtime,
notably for base classes and casting. For named tuples, using the new
class definition syntax introduced in Python 3.6 solves the issue.
In general, fixing the issue for *all* forward references requires
changing how module instantiation is performed in Python, from the
current single-pass top-down model. This would be a major change in the
language and is out of scope for this PEP.
Rejected Ideas
==============
Keep the ability to use local state when defining annotations
-------------------------------------------------------------
Keep the ability to use function 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::
With postponed evaluation, this would require keeping a reference to
the frame in which an annotation got created. This could be achieved
for example by storing all annotations as lambdas instead of strings.
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 would be prohibitively expensive for highly annotated code as the
frames would keep all their objects alive. That includes predominantly
objects that won't ever be accessed again.
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.
Note that in the case of nested classes, the functionality to get the
effective "globals" and "locals" at definition time is provided by
``typing.get_type_hints()``.
If a function generates a class or a function with annotations that
have to use local variables, it can populate the given generated
object's ``__annotations__`` dictionary directly, without relying on
the compiler.
Disallow local state usage for classes, too
-------------------------------------------
This PEP originally proposed limiting names within annotations to only
allow names from the model-level scope, including for classes. The
author argued this makes name resolution unambiguous, including in cases
of conflicts between local names and module-level names.
This idea was ultimately rejected in case of nested classes. Instead,
``typing.get_type_hints()`` got modified to populate the local namespace
correctly if class-level annotations are needed.
The reasons for rejecting the idea were that it goes against the
intuition of how scoping works in Python, and would break enough
existing type annotations to make the transition cumbersome. Finally,
local scope access is required for class decorators to be able to
evaluate type annotations. This is because class decorators are applied
before the class receives its name in the outer scope.
Introduce a new dictionary for the string literal form instead
--------------------------------------------------------------
Yury Selivanov shared the following idea:
1. Add a new special attribute to functions: ``__annotations_text__``.
2. Make ``__annotations__`` a lazy dynamic mapping, evaluating
expressions from the corresponding key in ``__annotations_text__``
just-in-time.
This idea is supposed to solve the backwards compatibility issue,
removing the need for a new ``__future__`` import. Sadly, this is not
enough. Postponed evaluation changes which state the annotation has
access to. While postponed evaluation fixes the forward reference
problem, it also makes it impossible to access function-level locals
anymore. This alone is a source of backwards incompatibility which
justifies a deprecation period.
A ``__future__`` import is an obvious and explicit indicator of opting
in for the new functionality. It also makes it trivial for external
tools to recognize the difference between a Python files using the old
or the new approach. In the former case, that tool would recognize that
local state access is allowed, whereas in the latter case it would
recognize that forward references are allowed.
Finally, just-in-time evaluation in ``__annotations__`` is an
unnecessary step if ``get_type_hints()`` is used later.
Drop annotations with -O
------------------------
There are two reasons this is not satisfying for the purpose of this
PEP.
First, this only addresses runtime cost, not forward references, those
still cannot be safely used in source code. A library maintainer would
never be able to use forward references since that would force the
library users to use this new hypothetical -O switch.
Second, this throws the baby out with the bath water. Now *no* runtime
annotation use can be performed. PEP 557 is one example of a recent
development where evaluating type annotations at runtime is useful.
All that being said, a granular -O option to drop annotations is
a possibility in the future, as it's conceptually compatible with
existing -O behavior (dropping docstrings and assert statements). This
PEP does not invalidate the idea.
Prior discussion
================
In PEP 484
----------
The forward reference problem was discussed when PEP 484 was originally
drafted, leading to the following statement in the document:
A compromise is possible where a ``__future__`` import could enable
turning *all* annotations in a given module into string literals, as
follows::
from __future__ import annotations
class ImSet:
def add(self, a: ImSet) -> List[ImSet]: ...
assert ImSet.add.__annotations__ == {
'a': 'ImSet', 'return': 'List[ImSet]'
}
Such a ``__future__`` import statement may be proposed in a separate
PEP.
python/typing#400
-----------------
The problem was discussed at length on the typing module's GitHub
project, under `Issue 400 <https://github.com/python/typing/issues/400>`_.
The problem statement there includes critique of generic types requiring
imports from ``typing``. This tends to be confusing to
beginners:
Why this::
from typing import List, Set
def dir(o: object = ...) -> List[str]: ...
def add_friends(friends: Set[Friend]) -> None: ...
But not this::
def dir(o: object = ...) -> list[str]: ...
def add_friends(friends: set[Friend]) -> None ...
Why this::
up_to_ten = list(range(10))
friends = set()
But not this::
from typing import List, Set
up_to_ten = List[int](range(10))
friends = Set[Friend]()
While typing usability is an interesting problem, it is out of scope
of this PEP. Specifically, any extensions of the typing syntax
standardized in PEP 484 will require their own respective PEPs and
approval.
Issue 400 ultimately suggests postponing evaluation of annotations and
keeping them as strings in ``__annotations__``, just like this PEP
specifies. This idea was received well. Ivan Levkivskyi supported
using the ``__future__`` import and suggested unparsing the AST in
``compile.c``. Jukka Lehtosalo pointed out that there are some cases
of forward references where types are used outside of annotations and
postponed evaluation will not help those. For those cases using the
string literal notation would still be required. Those cases are
discussed briefly in the "Forward References" section of this PEP.
The biggest controversy on the issue was Guido van Rossum's concern
that untokenizing annotation expressions back to their string form has
no precedent in the Python programming language and feels like a hacky
workaround. He said:
One thing that comes to mind is that it's a very random change to
the language. It might be useful to have a more compact way to
indicate deferred execution of expressions (using less syntax than
``lambda:``). But why would the use case of type annotations be so
all-important to change the language to do it there first (rather
than proposing a more general solution), given that there's already
a solution for this particular use case that requires very minimal
syntax?
Eventually, Ethan Smith and schollii voiced that feedback gathered
during PyCon US suggests that the state of forward references needs
fixing. Guido van Rossum suggested coming back to the ``__future__``
idea, pointing out that to prevent abuse, it's important for the
annotations to be kept both syntactically valid and evaluating correctly
at runtime.
First draft discussion on python-ideas
--------------------------------------
Discussion happened largely in two threads, `the original announcement
<https://mail.python.org/pipermail/python-ideas/2017-September/thread.html#47031>`_
and a follow-up called `PEP 563 and expensive backwards compatibility
<https://mail.python.org/pipermail/python-ideas/2017-September/thread.html#47108>`_.
The PEP received rather warm feedback (4 strongly in favor,
2 in favor with concerns, 2 against). The biggest voice of concern on
the former thread being Steven D'Aprano's review stating that the
problem definition of the PEP doesn't justify breaking backwards
compatibility. In this response Steven seemed mostly concerned about
Python no longer supporting evaluation of annotations that depended on
local function/class state.
A few people voiced concerns that there are libraries using annotations
for non-typing purposes. However, none of the named libraries would be
invalidated by this PEP. They do require adapting to the new
requirement to call ``eval()`` on the annotation with the correct
``globals`` and ``locals`` set.
This detail about ``globals`` and ``locals`` having to be correct was
picked up by a number of commenters. Nick Coghlan benchmarked turning
annotations into lambdas instead of strings, sadly this proved to be
much slower at runtime than the current situation.
The latter thread was started by Jim J. Jewett who stressed that
the ability to properly evaluate annotations is an important requirement
and backwards compatibility in that regard is valuable. After some
discussion he admitted that side effects in annotations are a code smell
and modal support to either perform or not perform evaluation is
a messy solution. His biggest concern remained loss of functionality
stemming from the evaluation restrictions on global and local scope.
Nick Coghlan pointed out that some of those evaluation restrictions from
the PEP could be lifted by a clever implementation of an evaluation
helper, which could solve self-referencing classes even in the form of a
class decorator. He suggested the PEP should provide this helper
function in the standard library.
Acknowledgements