PEP 749: Implementing PEP 649 (#3814)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
parent
e73affad48
commit
0a803fec89
|
@ -624,7 +624,8 @@ peps/pep-0742.rst @JelleZijlstra
|
|||
peps/pep-0743.rst @vstinner
|
||||
peps/pep-0744.rst @brandtbucher
|
||||
peps/pep-0745.rst @hugovk
|
||||
peps/pep-0746.rst @jellezijlstra
|
||||
peps/pep-0746.rst @JelleZijlstra
|
||||
peps/pep-0749.rst @JelleZijlstra
|
||||
# ...
|
||||
# peps/pep-0754.rst
|
||||
# ...
|
||||
|
|
|
@ -0,0 +1,753 @@
|
|||
PEP: 749
|
||||
Title: Implementing PEP 649
|
||||
Author: Jelle Zijlstra <jelle.zijlstra@gmail.com>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Topic: Typing
|
||||
Requires: 649
|
||||
Created: 28-May-2024
|
||||
Python-Version: 3.14
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This PEP supplements :pep:`649` by providing various tweaks and additions to its
|
||||
specification:
|
||||
|
||||
* ``from __future__ import annotations`` (:pep:`563`) will continue to exist with
|
||||
its current behavior at least until Python 3.13 reaches its end-of-life. Subsequently,
|
||||
it will be deprecated and eventually removed.
|
||||
* A new standard library module, ``annotations``, is added to provide tooling for
|
||||
annotations. It will include the ``get_annotations()`` function, an enum for annotation
|
||||
formats, a ``ForwardRef`` class, and a helper function for calling ``__annotate__`` functions.
|
||||
* Annotations in the REPL are lazily evaluated, just like other module-level annotations.
|
||||
* We specify the behavior of wrapper objects that provide annotations, such as :py:func:`classmethod`
|
||||
and code that uses :py:func:`functools.wraps`.
|
||||
* There will not be a code flag for marking ``__annotate__`` functions
|
||||
that can be run in a "fake globals" environment.
|
||||
* Setting the ``__annotations__`` attribute directly will not affect the ``__annotate__`` attribute.
|
||||
* We add functionality to allow evaluating type alias values and type parameter bounds and defaults
|
||||
(which were added by :pep:`695` and :pep:`696`) using PEP 649-like semantics.
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
:pep:`649` provides an excellent framework for creating better semantics for
|
||||
annotations in Python. It solves a common pain point for users of annotations,
|
||||
including those using static type hints as well as those using runtime typing,
|
||||
and it makes the language more elegant and powerful.
|
||||
The PEP was originally proposed in 2021 for Python 3.10,
|
||||
and it was accepted in 2023. However, the implementation took longer than anticipated,
|
||||
and now the PEP is expected to be implemented in Python 3.14.
|
||||
|
||||
I have started working on the implementation of the PEP in CPython. I found that
|
||||
the PEP leaves some areas underspecified, and some
|
||||
of its decisions in corner cases are questionable. This new PEP proposes several
|
||||
changes and additions to the specification to address these issues.
|
||||
|
||||
This PEP supplements rather than supersedes PEP 649. The changes proposed here
|
||||
should make the overall user experience better, but they do not change the
|
||||
general framework of the earlier PEP.
|
||||
|
||||
|
||||
The future of ``from __future__ import annotations``
|
||||
====================================================
|
||||
|
||||
:pep:`563` previously introduced the future import ``from __future__ import annotations``,
|
||||
which changes all annotations to strings. :pep:`649` proposes an alternative approach
|
||||
that does not require this future import, and states:
|
||||
|
||||
If this PEP is accepted, PEP 563 will be deprecated and eventually removed.
|
||||
|
||||
However, the PEP does not provide a detailed plan for this deprecation.
|
||||
|
||||
There is some previous discussion of this topic `on Discourse <https://discuss.python.org/t/pep-649-deferred-evaluation-of-annotations-tentatively-accepted/21331/44>`__
|
||||
(note that in the linked post I proposed something different from what is proposed here).
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
We suggest the following deprecation plan:
|
||||
|
||||
- In Python 3.14, ``from __future__ import annotations`` will continue to work as it
|
||||
did before, converting annotations into strings.
|
||||
- Sometime after the last release that did not support :pep:`649` semantics (expected to be 3.13)
|
||||
reaches its end-of-life, ``from __future__ import annotations`` is deprecated. Compiling
|
||||
any code that uses the future import will emit a :py:exc:`DeprecationWarning`. This will
|
||||
happen no sooner than the first release after Python 3.13 reaches its end-of-life, but
|
||||
the community may decide to wait longer.
|
||||
- After at least two releases, the future import is removed, and annotations are always
|
||||
evaluated as per :pep:`649`. Code that continues to use the future import will raise
|
||||
a :py:exc:`SyntaxError`, similar to any other undefined future import.
|
||||
|
||||
Rejected alternatives
|
||||
---------------------
|
||||
|
||||
*Immediately make the future import a no-op*: We considered applying :pep:`649` semantics
|
||||
to all code in Python 3.14, making the future import a no-op. However, this would break
|
||||
code that works in 3.13 under the following set of conditions:
|
||||
|
||||
* ``__future__ import annotations`` is active
|
||||
* There are annotations that rely on forward references
|
||||
* Annotations are eagerly evaluated at import time, for example by a metaclass or
|
||||
class or function decorator. For example, this currently applies to the
|
||||
released version of ``typing_extensions.TypedDict``.
|
||||
|
||||
This is expected to be a common pattern, so we cannot afford to break such code during
|
||||
the upgrade from 3.13 to 3.14.
|
||||
|
||||
Such code would still break when the future import is eventually removed. However, this
|
||||
is many years in the future, giving affected decorators plenty of time to update their code.
|
||||
|
||||
*Immediately deprecate the future import*: Instead of waiting until Python 3.13 reaches
|
||||
its end-of-life, we could immediately start emitting warnings when the future import is
|
||||
used. However, many libraries are already using ``from __future__ import annotations`` as
|
||||
an elegant way to enable unrestricted forward references in their annotations. If we deprecate
|
||||
the future import immediately, it would be impossible for these libraries to use unrestricted
|
||||
forward references on all supported Python versions while avoiding deprecation warnings:
|
||||
unlike other features deprecated from the standard library, a ``__future__`` import must
|
||||
be the first statement in a given module, meaning it would be impossible to only
|
||||
conditionally import ``__future__.annotations`` on Python 3.13 and lower. (The necessary
|
||||
``sys.version_info`` check would count as a statement preceding the ``__future__`` import.)
|
||||
|
||||
*Keep the future import around forever*: We could also decide to keep the future import
|
||||
indefinitely. However, this would permanently bifurcate the behavior of the Python
|
||||
language. This is undesirable; the language should have only a single set of semantics,
|
||||
not two permanently different modes.
|
||||
|
||||
New ``annotations`` module
|
||||
==========================
|
||||
|
||||
:pep:`649` proposes to add tooling related to annotations to the :py:mod:`inspect`
|
||||
module. However, that module is rather large, has direct or indirect dependencies
|
||||
on at least 35 other standard library modules, and is so slow to import that other
|
||||
standard library modules are often discouraged from importing it. Furthermore, we
|
||||
anticipate adding more tools in addition to the :py:func:`inspect.get_annotations`
|
||||
function and the ``VALUE``, ``FORWARDREF``, and ``SOURCE`` formats.
|
||||
|
||||
A new standard library module provides a logical home for this functionality and
|
||||
also enables us to add more tooling that is useful for consumers of annotations.
|
||||
|
||||
Rationale
|
||||
---------
|
||||
|
||||
:pep:`649` indicates that :py:class:`!typing.ForwardRef` should be used to implement the
|
||||
``FORWARDREF`` format in :py:func:`inspect.get_annotations`. However, the existing implementation
|
||||
of :py:class:`!typing.ForwardRef` is intertwined with the rest of the :py:mod:`!typing` module,
|
||||
and it would not make sense to add :py:mod:`!typing`-specific behavior to the generic ``get_annotations()``
|
||||
function. Furthermore, :py:class:`!typing.ForwardRef` is a problematic
|
||||
class: it is public and documented, but the documentation lists no attributes or methods
|
||||
for it. Nonetheless, third-party libraries make use of some of its undocumented
|
||||
attributes. For instance, `Pydantic <https://github.com/pydantic/pydantic/blob/00ff77ed37589d924d3c10e0d5a48a7ef679a0d7/pydantic/v1/typing.py#L66>`__
|
||||
and `Typeguard <https://github.com/agronholm/typeguard/blob/016f8139f5a0a63147d68df9558cc5584cd2c49a/src/typeguard/_utils.py#L44>`__
|
||||
use the ``_evaluate`` method; `beartype <https://github.com/beartype/beartype/blob/0b4453f83c7ed4be054d8733aab8075e1478e166/beartype/_util/hint/pep/proposal/pep484585/utilpep484585ref.py#L210>`__
|
||||
and `pyanalyze <https://github.com/quora/pyanalyze/blob/9e401724f9d035cf138b72612834b6d5a00eb8e8/pyanalyze/annotations.py#L509>`__
|
||||
use the ``__forward_arg__`` attribute.
|
||||
|
||||
We replace the existing but poorly specified :py:class:`!typing.ForwardRef` with a new class,
|
||||
``annotations.ForwardRef``. It is designed to be mostly compatible with existing uses
|
||||
of the :py:class:`!typing.ForwardRef` class, but without the behaviors specific to the
|
||||
:py:mod:`!typing` module. For compatibility with existing users, we keep the private
|
||||
``_evaluate`` method, but mark it as deprecated. It delegates to a new public function in
|
||||
the :py:mod:`!typing` module, ``typing.evaluate_forward_ref``, that is designed to
|
||||
evaluate forward references in a way that is specific to type hints.
|
||||
|
||||
We add a function ``annotations.call_annotate_function`` as a helper for calling
|
||||
``__annotate__`` functions. This is a useful building block when implementing functionality
|
||||
that needs to partially evaluate annotations while a class is being constructed.
|
||||
For example, the implementation of :py:class:`typing.NamedTuple` needs to retrieve
|
||||
the annotations from a class namespace dictionary before the namedtuple class itself
|
||||
can be constructed, because the annotations determine what fields exist on the namedtuple.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
A new module, ``annotations``, is added to the standard library. Its aim is to
|
||||
provide tooling for introspecting and wrapping annotations.
|
||||
|
||||
The exact contents of the module are not yet specified. We will add support for
|
||||
:pep:`649` semantics to standard library functionality that uses annotations, such
|
||||
as :py:mod:`dataclasses` and :py:class:`typing.TypedDict`, and use the experience
|
||||
to inform the design of the new module.
|
||||
|
||||
The module will contain the following functionality:
|
||||
|
||||
* ``get_annotations()``: A function that returns the annotations of a function,
|
||||
module, or class. This will replace :py:func:`inspect.get_annotations`. The latter
|
||||
will delegate to the new function. It may eventually be deprecated, but to
|
||||
minimize disruption, we do not propose an immediate deprecation.
|
||||
* ``Format``: an enum that contains the possible formats of annotations. This will
|
||||
replace the ``VALUE``, ``FORWARDREF``, and ``SOURCE`` formats in :pep:`649`.
|
||||
PEP 649 proposed to make these values global members of the :py:mod:`inspect`
|
||||
module; we prefer to place them within an enum.
|
||||
* ``ForwardRef``: a class representing a forward reference; it may be returned by
|
||||
``get_annotations()`` when the format is ``FORWARDREF``. The existing class
|
||||
:py:class:`typing.ForwardRef` will become an alias of this class. Its members include:
|
||||
|
||||
* ``__forward_arg__``: the string argument of the forward reference
|
||||
* ``evaluate(globals=None, locals=None, type_params=None, owner=None)``: a method that attempts to evaluate
|
||||
the forward reference. The ``ForwardRef`` object may hold a reference to the
|
||||
globals and other namespaces of the object that it originated from. If so, these
|
||||
namespaces may be used to evaluate the forward reference. The *owner* argument
|
||||
may be the object that holds the original annotation, such as the class or module
|
||||
object; it is used to extract the globals and locals namespaces if these are not
|
||||
provided.
|
||||
* ``_evaluate()``, with the same interface as the existing ``ForwardRef._evaluate``
|
||||
method. It will be undocumented and immediately deprecated. It is provided for
|
||||
compatibility with existing users of ``typing.ForwardRef``.
|
||||
|
||||
* ``call_annotate_function(func: Callable, format: Format)``: a helper for calling
|
||||
an ``__annotate__`` function with a given format. If the function does not support
|
||||
this format, ``call_annotate_function()`` will set up a "fake globals" environment,
|
||||
as described in :pep:`649`, and use that environment to return the desired annotations
|
||||
format.
|
||||
* ``call_evaluate_function(func: Callable | None, format: Format)``: similar to
|
||||
``call_annotate_function``, but does not rely on the function returning an annotations
|
||||
dictionary. This is intended to be used for evaluating deferred attributes introduced by
|
||||
:pep:`695` and :pep:`696`; see below for details. *func* may be ``None``
|
||||
for convenience; if ``None`` is passed, the function also returns ``None``.
|
||||
|
||||
A new function is also added to the :py:mod:`!typing` module, ``typing.evaluate_forward_ref``.
|
||||
This function is a wrapper around the ``ForwardRef.evaluate`` method, but it performs
|
||||
additional work that is specific to type hints. For example, it recurses into complex
|
||||
types and evaluates additional forward references within these types.
|
||||
|
||||
Open issues
|
||||
-----------
|
||||
|
||||
What should this module be called? Some ideas:
|
||||
|
||||
- ``annotations``: The most obvious name, but it may cause confusion with the existing
|
||||
``from __future__ import annotations``. There is a PyPI package :pypi:`annotations`,
|
||||
but it had only a single release in 2015 and looks abandoned.
|
||||
- ``annotools``: Analogous to :py:mod:`itertools` and :py:mod:`functools`, but "anno" is a less
|
||||
obvious abbreviation than "iter" or "func". As of this writing, there
|
||||
is no PyPI package with this name.
|
||||
- ``annotationtools``: A more explicit version. There is a PyPI package
|
||||
:pypi:`annotationtools`, which had a release in 2023.
|
||||
- ``annotation_tools``: A variation of the above but without a PyPI conflict. However,
|
||||
no other public standard library module has an underscore in its name.
|
||||
- ``annotationslib``: Analogous to :py:mod:`tomllib`, :py:mod:`pathlib`, and :py:mod:`importlib`.
|
||||
There is no PyPI package with this name.
|
||||
|
||||
Rejected alternatives
|
||||
---------------------
|
||||
|
||||
*Add the functionality to the inspect module*: As described above, the
|
||||
:py:mod:`inspect` module is already quite large, and its import time is prohibitive
|
||||
for some use cases.
|
||||
|
||||
*Add the functionality to the typing module*: While annotations are mostly
|
||||
used for typing, they may also be used for other purposes. We prefer to keep a clean
|
||||
separation between functionality for introspecting annotations and functionality that
|
||||
is exclusively meant for type hints.
|
||||
|
||||
*Add the functionality to the types module*: The :py:mod:`types` module is
|
||||
meant for functionality related to *types*, and annotations can exist on functions
|
||||
and modules, not only on types.
|
||||
|
||||
*Develop this functionality in a third-party package*: The functionality in this new
|
||||
module will be pure Python code, and it is possible to implement a third-party package
|
||||
that provides the same functionality by interacting directly with ``__annotate__``
|
||||
functions generated by the interpreter. However, the functionality of the proposed new
|
||||
module will certainly be useful in the standard library itself (e.g., for implementing
|
||||
:py:mod:`dataclasses` and :py:class:`typing.NamedTuple`), so it makes sense to include
|
||||
it in the standard library.
|
||||
|
||||
*Add this functionality to a private module*: It would be possible to initially develop
|
||||
the module in a private standard library module (e.g., ``_annotations``), and publicize
|
||||
it only after we have gained more experience with the API. However, we already know
|
||||
that we will need parts of this module for the standard library itself (e.g., for
|
||||
implementing :py:mod:`!dataclasses` and :py:class:`!typing.NamedTuple`). Even if we make
|
||||
it private, the module will inevitably get used by third-party users. It is preferable
|
||||
to start with a clear, documented API from the beginning, to enable third-party users to
|
||||
support :pep:`649` semantics as thoroughly as the standard library. The module will
|
||||
immediately be used in other parts of the standard library, ensuring that it covers a
|
||||
reasonable set of use cases.
|
||||
|
||||
Behavior of the REPL
|
||||
====================
|
||||
|
||||
:pep:`649` specifies the following behavior of the interactive REPL:
|
||||
|
||||
For the sake of simplicity, in this case we forego delayed evaluation.
|
||||
Module-level annotations in the REPL shell will continue to work exactly
|
||||
as they do with “stock semantics”, evaluating immediately and setting the
|
||||
result directly inside the ``__annotations__`` dict.
|
||||
|
||||
There are several problems with this proposed behavior. It makes the REPL the
|
||||
only context where annotations are still evaluated immediately, which is
|
||||
confusing for users and complicates the language.
|
||||
|
||||
It also makes the implementation of the REPL more complex, as it needs to
|
||||
ensure that all statements are compiled in "interactive" mode, even if their
|
||||
output does not need to be displayed. (This matters if there are multiple
|
||||
statements in a single line evaluated by the REPL.)
|
||||
|
||||
Most importantly, this breaks some plausible use cases that inexperienced
|
||||
users could run into. A user might write the following in a file::
|
||||
|
||||
a: X | None = None
|
||||
class X: ...
|
||||
|
||||
Under :pep:`649` this would work fine: ``X`` is not yet defined when it is used
|
||||
in the annotation for ``a``, but the annotation is lazily evaluated. However,
|
||||
if a user were to paste this same code into the REPL and execute it line by
|
||||
line, it would throw a ``NameError``, because the name ``X`` is not yet defined.
|
||||
|
||||
This topic was previously discussed `on Discourse <https://discuss.python.org/t/pep-649-behavior-of-the-repl/54109>`__.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
We propose to treat the interactive console like any other module-level code, and
|
||||
make annotations lazily evaluated. This makes the language more consistent and
|
||||
avoids subtle behavior changes between modules and the REPL.
|
||||
|
||||
Because the REPL is evaluated line by line, we would generate a new ``__annotate__``
|
||||
function for every evaluated statement in the global scope that contains annotations. Whenever a line
|
||||
containing annotations is evaluated, the previous ``__annotate__`` function is
|
||||
lost:
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> x: int
|
||||
>>> __annotate__(1)
|
||||
{'x': <class 'int'>}
|
||||
>>> y: str
|
||||
>>> __annotate__(1)
|
||||
{'y': <class 'str'>}
|
||||
>>> z: doesntexist
|
||||
>>> __annotate__(1)
|
||||
Traceback (most recent call last):
|
||||
File "<python-input-5>", line 1, in <module>
|
||||
__annotate__(1)
|
||||
~~~~~~~~~~~~^^^
|
||||
File "<python-input-4>", line 1, in __annotate__
|
||||
z: doesntexist
|
||||
^^^^^^^^^^^
|
||||
NameError: name 'doesntexist' is not defined
|
||||
|
||||
There will be no ``__annotations__`` key in the global namespace of the REPL.
|
||||
In module namespaces, this key is created lazily when the ``__annotations__``
|
||||
descriptor of the module object is accessed, but in the REPL there is no such module
|
||||
object.
|
||||
|
||||
Classes and functions defined within the REPL will also work like any other classes,
|
||||
so evaluation of their annotations will be deferred. It is possible to access the
|
||||
``__annotations__`` and ``__annotate__`` attributes or use the ``annotations`` module
|
||||
to introspect the annotations.
|
||||
|
||||
Wrappers that provide ``__annotations__``
|
||||
=========================================
|
||||
|
||||
Several objects in the standard library and elsewhere provide annotations for their
|
||||
wrapped object. :pep:`649` does not specify how such wrappers should behave.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
Wrappers that provide annotations should be designed with the following goals
|
||||
in mind:
|
||||
|
||||
* Evaluation of ``__annotations__`` should be deferred for as long as possible,
|
||||
consistent with the behavior of built-in functions, classes, and modules.
|
||||
* Backward compatibility with the behavior prior to the implementation of :pep:`649`
|
||||
should be preserved.
|
||||
* The ``__annotate__`` and ``__annotations__`` attributes should both be supplied
|
||||
with semantics consistent to those of the wrapped object.
|
||||
|
||||
More specifically:
|
||||
|
||||
* :py:func:`functools.update_wrapper` (and therefore :py:func:`functools.wraps`)
|
||||
will copy only the ``__annotate__`` attribute
|
||||
from the wrapped object to the wrapper. The ``__annotations__`` descriptor on the
|
||||
wrapper function will use the copied ``__annotate__``.
|
||||
* The constructors for :py:func:`classmethod` and :py:func:`staticmethod` currently
|
||||
copy the ``__annotations__`` attribute from the wrapped object to the wrapper.
|
||||
They will instead have writable attributes for
|
||||
``__annotate__`` and ``__annotations__``. Reading these attributes will retrieve
|
||||
the corresponding attribute from the underlying callable and cache it in the wrapper's
|
||||
``__dict__``. Writing to these attributes will directly update the ``__dict__``,
|
||||
without affecting the wrapped callable.
|
||||
|
||||
Remove code flag for marking ``__annotate__`` functions
|
||||
=======================================================
|
||||
|
||||
:pep:`649` specifies:
|
||||
|
||||
This PEP assumes that
|
||||
third-party libraries may implement their own ``__annotate__``
|
||||
methods, and those functions would almost certainly work
|
||||
incorrectly when run in this "fake globals" environment.
|
||||
For that reason, this PEP allocates a flag on code objects,
|
||||
one of the unused bits in ``co_flags``, to mean "This code
|
||||
object can be run in a 'fake globals' environment." This
|
||||
makes the "fake globals" environment strictly opt-in, and
|
||||
it's expected that only ``__annotate__`` methods generated
|
||||
by the Python compiler will set it.
|
||||
|
||||
We have not found a need for this mechanism during our work to
|
||||
add :pep:`649` support to the standard library. While it is true
|
||||
that custom ``__annotate__`` functions may not work well with the
|
||||
"fake globals" environment, this technique is used only when the
|
||||
``__annotate__`` function raises :py:exc:`NotImplementedError` to
|
||||
signal that it does not support the requested format. However,
|
||||
manually implemented ``__annotate__`` functions are likely to support
|
||||
all three annotation formats; often, they will consist of a call to
|
||||
``annotations.call_annotate_function`` plus some transformation of the
|
||||
result.
|
||||
|
||||
In addition, the proposed mechanism couples the implementation with
|
||||
low-level details of the code object. The code object flags are
|
||||
CPython-specific and the documentation :py:ref:`explicitly warns <inspect-module-co-flags>`
|
||||
against relying on the values.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
The standard library will use the "fake globals" technique on any
|
||||
``__annotate__`` function that raises :py:exc:`NotImplementedError`
|
||||
when the requested format is not supported.
|
||||
|
||||
Third-party code that implements ``__annotate__`` functions should either
|
||||
support all three annotation formats, or be prepared to handle the
|
||||
"fake globals" environment. This should be mentioned in the data model
|
||||
documentation for ``__annotate__``.
|
||||
|
||||
Effect of setting ``__annotations__``
|
||||
=====================================
|
||||
|
||||
:pep:`649` specifies:
|
||||
|
||||
Setting ``o.__annotations__`` to a legal value
|
||||
automatically sets ``o.__annotate__`` to ``None``.
|
||||
|
||||
We would prefer to keep ``__annotate__`` unchanged when ``__annotations__``
|
||||
is written to. Conceptually, ``__annotate__`` provides the ground truth
|
||||
and ``__annotations__`` is merely a cache, and we shouldn't throw away the
|
||||
ground truth if the cache is modified.
|
||||
|
||||
The motivation for :pep:`649`'s behavior is to keep the two attributes in sync.
|
||||
However, this is impossible in general; if the ``__annotations__`` dictionary
|
||||
is modified in place, this will not be reflected in the ``__annotate__`` attribute.
|
||||
The overall mental model for this area will be simpler if setting ``__annotations__``
|
||||
has no effect on ``__annotate__``.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
The value of ``__annotate__`` is not changed when ``__annotations__`` is set.
|
||||
|
||||
Deferred evaluation of PEP 695 and 696 objects
|
||||
==============================================
|
||||
|
||||
Since :pep:`649` was written, Python 3.12 and 3.13 gained support for
|
||||
several new features that also use deferred evaluation, similar to the
|
||||
behavior this PEP proposes for annotations:
|
||||
|
||||
* The value of type aliases created through the :py:keyword:`type`
|
||||
statement (:pep:`695`)
|
||||
* The bound and constraints of :py:class:`typing.TypeVar` objects
|
||||
created through the syntax for generics (:pep:`695`)
|
||||
* The default value of :py:class:`typing.TypeVar`, :py:class:`ParamSpec`,
|
||||
and :py:class:`typing.TypeVarTuple` objects (:pep:`696`)
|
||||
|
||||
Currently, these objects use deferred evaluation, but there is no direct
|
||||
access to the function object used for deferred evaluation. To enable
|
||||
the same kind of introspection that is now possible for annotations, we propose
|
||||
to expose the internal function objects, allowing users to evaluate them
|
||||
using the FORWARDREF and SOURCE formats.
|
||||
|
||||
Specification
|
||||
-------------
|
||||
|
||||
We will add the following new attributes:
|
||||
|
||||
* ``evaluate_value`` on :py:class:`typing.TypeAliasType`
|
||||
* ``evaluate_bound``, ``evaluate_constraints``, and ``evaluate_default`` on :py:class:`typing.TypeVar`
|
||||
* ``evaluate_default`` on :py:class:`typing.ParamSpec`
|
||||
* ``evaluate_default`` on :py:class:`typing.TypeVarTuple`
|
||||
|
||||
Except for ``evaluate_value``, these attributes may be ``None`` if the object
|
||||
does not have a bound, constraints, or default. Otherwise, the attribute is a
|
||||
callable, similar to an ``__annotate__`` function, that takes a single integer
|
||||
argument and returns the evaluated value. Unlike ``__annotate__`` functions,
|
||||
these callables return a single value, not a dictionary of annotations.
|
||||
These attributes are read-only.
|
||||
|
||||
Usually, users would use these attributes in combinations with
|
||||
``annotations.call_evaluate_function``. For example, to get a ``TypeVar``'s bound
|
||||
in SOURCE format, one could write
|
||||
``annotations.call_evaluate_function(T.evaluate_bound, annotations.Format.SOURCE)``.
|
||||
|
||||
Miscellaneous implementation details
|
||||
====================================
|
||||
|
||||
:pep:`649` goes into considerable detail on some aspects of the implementation.
|
||||
To avoid confusion, we describe a few aspects where the current implementation
|
||||
differs from that described in the PEP. However, these details are not guaranteed
|
||||
to hold in the future, and they may change without notice in the future, unless
|
||||
they are documented in the language reference.
|
||||
|
||||
Supported operations on ``ForwardRef`` objects
|
||||
----------------------------------------------
|
||||
|
||||
The ``SOURCE`` format is implemented by the "stringizer" technique,
|
||||
where the globals dictionary of a function is augmented so that every
|
||||
lookup results in a special object that can be used to reconstruct the
|
||||
operations that are performed on the object.
|
||||
|
||||
:pep:`649` specifies:
|
||||
|
||||
In practice, the "stringizer" functionality will be implemented
|
||||
in the ``ForwardRef`` object currently defined in the
|
||||
``typing`` module. ``ForwardRef`` will be extended to
|
||||
implement all stringizer functionality; it will also be
|
||||
extended to support evaluating the string it contains,
|
||||
to produce the real value (assuming all symbols referenced
|
||||
are defined).
|
||||
|
||||
However, this is likely to lead to confusion in practice. An object
|
||||
that implements stringizer functionality must implement almost all
|
||||
special methods, including ``__getattr__`` and ``__eq__``, to return
|
||||
a new stringizer. Such an object is confusing to work with: all operations
|
||||
succeed, but they are likely to return different objects than the user
|
||||
expects.
|
||||
|
||||
The current implementation instead implements only a few useful methods
|
||||
on the ``ForwardRef`` class. During the evaluation of annotations,
|
||||
an instance of a private stringizer class is used instead of ``ForwardRef``.
|
||||
After evaluation completes, the implementation of the FORWARDREF format
|
||||
converts these internal objects into ``ForwardRef`` objects.
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
:pep:`649` provides a thorough discussion of the backwards compatibility implications
|
||||
on existing code that uses either stock or :pep:`563` semantics.
|
||||
|
||||
However, there is another set of compatibility problems: new code that is written
|
||||
assuming :pep:`649` semantics, but uses existing tools that eagerly evaluate annotations.
|
||||
For example, consider a ``dataclass``-like class decorator ``@annotator`` that retrieves the annotated
|
||||
fields in the class it decorates, either by accessing ``__annotations__`` directly
|
||||
or by calling :py:func:`inspect.get_annotations`.
|
||||
|
||||
Once :pep:`649` is implemented, code like this will work fine::
|
||||
|
||||
class X:
|
||||
y: Y
|
||||
|
||||
class Y: pass
|
||||
|
||||
But this will not, unless ``@annotator`` is changed to use the new ``FORWARDREF``
|
||||
format::
|
||||
|
||||
@annotator
|
||||
class X:
|
||||
y: Y
|
||||
|
||||
class Y: pass
|
||||
|
||||
This is not strictly a backwards compatibility issue, since no previously working code
|
||||
would break; before :pep:`649`, this code would have raised ``NameError`` at runtime.
|
||||
In a sense, it is no different from any other new Python feature that needs
|
||||
to be supported by third-party libraries. Nevertheless, it is a serious issue for libraries
|
||||
that perform introspection, and it is important that we make it as easy as possible for
|
||||
libraries to support the new semantics in a straightforward, user-friendly way.
|
||||
|
||||
We will update those parts of the standard library that are affected by this problem,
|
||||
and we propose to add commonly useful functionality to the new ``annotations`` module,
|
||||
so third-party tools can use the same set of tools.
|
||||
|
||||
|
||||
Security Implications
|
||||
=====================
|
||||
|
||||
None.
|
||||
|
||||
|
||||
How to Teach This
|
||||
=================
|
||||
|
||||
The semantics of :pep:`649`, as modified by this PEP, should largely be intuitive for
|
||||
users who add annotations to their code. We eliminate the need for manually adding
|
||||
quotes around annotations that require forward references, a major source of confusion
|
||||
for users.
|
||||
|
||||
For advanced users who need to introspect annotations, the story becomes more complex.
|
||||
The documentation of the new ``annotations`` module will serve as a reference for users
|
||||
who need to interact programmatically with annotations.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
The in-progress PR `#119891 <https://github.com/python/cpython/pull/119891>`__
|
||||
implements much of this PEP.
|
||||
|
||||
Open Issues
|
||||
===========
|
||||
|
||||
We may discover additional areas where :pep:`649` needs clarification or amendment
|
||||
as we make progress on implementing it. Readers are encouraged to follow the
|
||||
`CPython issue <https://github.com/python/cpython/issues/119180>`__ tracking the
|
||||
implementation of the PEP and try out the draft implementation. Any feedback may
|
||||
be incorporated into future versions of this PEP.
|
||||
|
||||
Should dataclass field types use deferred evaluation?
|
||||
-----------------------------------------------------
|
||||
|
||||
The current draft implementation already supports deferred evaluation in dataclasses,
|
||||
so this works:
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> from dataclasses import dataclass
|
||||
>>> @dataclass
|
||||
... class D:
|
||||
... x: undefined
|
||||
...
|
||||
|
||||
However, the ``FORWARDREF`` format leaks into the field types of the dataclass:
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> fields(D)[0].type
|
||||
ForwardRef('undefined')
|
||||
|
||||
We could instead add deferred evaluation for the field type, similar to that outlined
|
||||
above for type alias values.
|
||||
|
||||
Accessing ``.type`` might throw an error:
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> @dataclass
|
||||
... class D:
|
||||
... x: undefined
|
||||
...
|
||||
>>> field = fields(D)[0]
|
||||
>>> field.type
|
||||
Traceback (most recent call last):
|
||||
File "<python-input-4>", line 1, in <module>
|
||||
field.type
|
||||
File ".../dataclasses.py", line 308, in type
|
||||
annos = self._annotate(annotations.Format.VALUE)
|
||||
File "<python-input-2>", line 3, in __annotate__
|
||||
x: undefined
|
||||
^^^^^^^^^
|
||||
NameError: name 'undefined' is not defined
|
||||
|
||||
But users could use ``annotations.call_evaluate_function`` to get the type in other formats:
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> annotations.call_evaluate_function(field.evaluate_type, annotations.Format.SOURCE)
|
||||
'undefined'
|
||||
>>> annotations.call_evaluate_function(field.evaluate_type, annotations.Format.FORWARDREF)
|
||||
ForwardRef('undefined')
|
||||
|
||||
Other variations are possible. For example, we could leave the ``type`` attribute unchanged,
|
||||
and only add the ``evaluate_type`` method. This avoids unpleasant surprises where accessing
|
||||
``.type`` may throw an exception.
|
||||
|
||||
Acknowledgments
|
||||
===============
|
||||
|
||||
First of all, I thank Larry Hastings for writing :pep:`649`. This PEP modifies some of his
|
||||
initial decisions, but the overall design is still his.
|
||||
|
||||
I thank Carl Meyer and Alex Waygood for feedback on early drafts of this PEP.
|
||||
|
||||
Appendix
|
||||
========
|
||||
|
||||
Which expressions can be stringified?
|
||||
-------------------------------------
|
||||
|
||||
:pep:`649` acknowledges that the stringifier cannot handle all expressions. Now that we
|
||||
have a draft implementation, we can be more precise about the expressions that can and
|
||||
cannot be handled. Below is a list of all expressions in the Python AST that can and
|
||||
cannot be recovered by the stringifier. The full list should probably not be added to
|
||||
the documentation, but creating it is a useful exercise.
|
||||
|
||||
First, the stringifier of course cannot recover any information that is not present in
|
||||
the compiled code, including comments, whitespace, parenthesization, and operations that
|
||||
get simplified by the AST optimizer.
|
||||
|
||||
Second, the stringifier can intercept almost all operations that involve names looked
|
||||
up in some scope, but it cannot intercept operations that operate fully on constants.
|
||||
As a corollary, this also means it is not safe to request the ``SOURCE`` format on
|
||||
untrusted code: Python is powerful enough that it is possible to achieve arbitrary
|
||||
code execution even with no access to any globals or builtins. For example:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
|
||||
...
|
||||
>>> annotations.get_annotations(f, format=annotations.Format.SOURCE)
|
||||
Hello world
|
||||
{'x': 'None'}
|
||||
|
||||
(This particular example worked for me on the current implementation of a draft of this PEP;
|
||||
the exact code may not keep working in the future.)
|
||||
|
||||
The following are supported (sometimes with caveats):
|
||||
|
||||
* ``BinOp``
|
||||
* ``UnaryOp``
|
||||
|
||||
* ``Invert`` (``~``), ``UAdd`` (``+``), and ``USub`` (``-``) are supported
|
||||
* ``Not`` (``not``) is not supported
|
||||
|
||||
* ``Dict`` (except when using ``**`` unpacking)
|
||||
* ``Set``
|
||||
* ``Compare``
|
||||
|
||||
* ``Eq`` and ``NotEq`` are supported
|
||||
* ``Lt``, ``LtE``, ``Gt``, and ``GtE`` are supported, but the operand may be flipped
|
||||
* ``Is``, ``IsNot``, ``In``, and ``NotIn`` are not supported
|
||||
|
||||
* ``Call`` (except when using ``**`` unpacking)
|
||||
* ``Constant`` (though not the exact representation of the constant; for example, escape
|
||||
sequences in strings are lost; hexadecimal numbers are converted to decimal)
|
||||
* ``Attribute`` (assuming the value is not a constant)
|
||||
* ``Subscript`` (assuming the value is not a constant)
|
||||
* ``Starred`` (``*`` unpacking)
|
||||
* ``Name``
|
||||
* ``List``
|
||||
* ``Tuple``
|
||||
* ``Slice``
|
||||
|
||||
The following are unsupported, but throw an informative error when encountered by the
|
||||
stringifier:
|
||||
|
||||
* ``FormattedValue`` (f-strings; error is not detected if conversion specifiers like ``!r``
|
||||
are used)
|
||||
* ``JoinedStr`` (f-strings)
|
||||
|
||||
The following are unsupported and result in incorrect output:
|
||||
|
||||
* ``BoolOp`` (``and`` and ``or``)
|
||||
* ``IfExp``
|
||||
* ``Lambda``
|
||||
* ``ListComp``
|
||||
* ``SetComp``
|
||||
* ``DictComp``
|
||||
* ``GeneratorExp``
|
||||
|
||||
The following are disallowed in annotation scopes and therefore not relevant:
|
||||
|
||||
* ``NamedExpr`` (``:=``)
|
||||
* ``Await``
|
||||
* ``Yield``
|
||||
* ``YieldFrom``
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document is placed in the public domain or under the
|
||||
CC0-1.0-Universal license, whichever is more permissive.
|
Loading…
Reference in New Issue