Skip to content

Commit aeaae7f

Browse files
committed
PEP 749: Add conditional annotations and partially executed modules
1 parent 0a4a820 commit aeaae7f

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed

peps/pep-0749.rst

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ specification:
3535
(which were added by :pep:`695` and :pep:`696`) using PEP 649-like semantics.
3636
* The ``SOURCE`` format is renamed to ``STRING`` to improve clarity and reduce the risk of
3737
user confusion.
38+
* Conditionally defined class and module annotations are handled correctly.
39+
* Accessing annotations on a partially executed module will raise :py:exc:`RuntimeError`.
3840

3941
Motivation
4042
==========
@@ -752,6 +754,125 @@ PEP, the four supported formats are now:
752754
- ``FORWARDREF``: replaces undefined names with ``ForwardRef`` objects.
753755
- ``STRING``: returns strings, attempts to recreate code close to the original source.
754756

757+
Conditionally defined annotations
758+
=================================
759+
760+
:pep:`649` does not support annotations that are conditionally defined
761+
in the body of a class or module:
762+
763+
It's currently possible to set module and class attributes with
764+
annotations inside an ``if`` or ``try`` statement, and it works
765+
as one would expect. It's untenable to support this behavior
766+
when this PEP is active.
767+
768+
However, the maintainer of the widely used SQLAlchemy library
769+
`reported <https://github.com/python/cpython/issues/130881>`__
770+
that this pattern is actually common and important:
771+
772+
.. code:: python
773+
774+
from typing import TYPE_CHECKING
775+
776+
if TYPE_CHECKING:
777+
from some_module import SpecialType
778+
779+
class MyClass:
780+
somevalue: str
781+
if TYPE_CHECKING:
782+
someothervalue: SpecialType
783+
784+
Under the behavior envisioned in :pep:`649`, the ``__annotations__`` for
785+
``MyClass`` would contain keys for both ``somevalue`` and ``someothervalue``.
786+
787+
Fortunately, there is a tractable implementation strategy for making
788+
this code behave as expected again. This strategy relies on a few fortuitous
789+
circumstances:
790+
791+
* This behavior change is only relevant to module and class annotations,
792+
because annotations in local scopes are ignored.
793+
* Module and class bodies are only executed once.
794+
* The annotations of a class are not externally visible until execution of the
795+
class body is complete. For modules, this is not quite true, because a partially
796+
executed module can be visible to other imported modules, but this is an
797+
unusual case that is problematic for other reasons (see the next section).
798+
799+
This allows the following implementation strategy:
800+
801+
* Each annotated assignment is assigned a unique identifier (e.g., an integer).
802+
* During execution of a class or module body, a set, initially empty, is created
803+
to hold the identifiers of the annotations that have been defined.
804+
* When an annotated assignment is executed, its identifier is added to the set.
805+
* The generated ``__annotate__`` function uses the set to determine
806+
which annotations were defined in the class or module body, and return only those.
807+
808+
This was implemented in :gh:pr:`130935`.
809+
810+
Specification
811+
-------------
812+
813+
For classes and modules, the ``__annotate__`` function will return only
814+
annotations for those assignments that were executed when the class or module body
815+
was executed.
816+
817+
Caching of annotations on partially executed modules
818+
====================================================
819+
820+
:pep:`649` specifies that the value of the ``__annotations__`` attribute
821+
on classes and modules is determined on first access by calling the
822+
``__annotate__`` function, and then it is cached for later access.
823+
This is correct in most cases and preserves compatibility, but there is
824+
one edge case where it can lead to surprising behavior: partially executed
825+
modules.
826+
827+
Consider this example:
828+
829+
.. code:: python
830+
831+
# recmod/__main__.py
832+
from . import a
833+
print("in __main__:", a.__annotations__)
834+
835+
# recmod/a.py
836+
v1: int
837+
from . import b
838+
v2: int
839+
840+
# recmod/b.py
841+
from . import a
842+
print("in b:", a.__annotations__)
843+
844+
Note that while ``.py`` executes, the ``recmod.a`` module is defined,
845+
but has not yet finished execution.
846+
847+
On 3.13, this produces:
848+
849+
.. code:: shell
850+
851+
$ python3.13 -m recmod
852+
in b: {'v1': <class 'int'>}
853+
in __main__: {'v1': <class 'int'>, 'v2': <class 'int'>}
854+
855+
But with :pep:`649` implemented as originally proposed, this would
856+
print an empty dictionary twice, because the ``__annotate__`` function
857+
is set only when module execution is complete. This is obviously
858+
unintuitive.
859+
860+
See :gh:issue:`130907` for implementation.
861+
862+
Specification
863+
-------------
864+
865+
Accessing ``__annotations__`` on a partially executed module will
866+
raise :py:exc:`RuntimeError`. After module execution is complete,
867+
accessing ``__annotations__`` will execute and cache the annotations as
868+
normal.
869+
870+
This is technically a compatibility break for code that introspects
871+
annotations on partially executed modules, but that should be a rare
872+
case. It is better to couple this compatibility break with the other
873+
changes in annotations behavior introduced by this PEP and :pep:`649`.
874+
875+
755876
Miscellaneous implementation details
756877
====================================
757878

0 commit comments

Comments
 (0)