First public draft to be discussed on python-dev.
This commit is contained in:
parent
cbe3e2d540
commit
adf528d4d0
433
pep-0563.rst
433
pep-0563.rst
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue