PEP 649 revisited
Back in June, we looked at a change to Python annotations, which provide a way to associate metadata, such as type information, with functions. That change was planned for the upcoming Python 3.10 release, but was deferred due to questions about it and its impact on run-time uses of the feature. The Python steering council felt that more time was needed to consider all of the different aspects of the problem before deciding on the right approach; the feature freeze for Python 3.10 was only around two weeks off when the decision was announced on April 20. But now, there is most of a year before another feature freeze, which gives the council (and the greater Python development community) some time to discuss it at a more leisurely pace.
To that end, Eric V. Smith raised
the issue on the python-dev mailing list on August 9. He did so in
the context of PEP 649
("Deferred Evaluation Of Annotations Using Descriptors
"),
which was the late-breaking proposal that caused the original plan to be
put on hold. That plan was embodied in PEP 563
("Postponed Evaluation of Annotations
"), which was accepted
back in 2017 and was set to become the default—and only—behavior for
annotations starting in Python 3.10. The council decided to defer the
change in the default until Python 3.11 at the earliest and there is
the possibility of switching
to the behavior described in PEP 649 instead. Smith wanted to see if
the issue could be resolved at this point.
Backstory
The history was described at some length in our earlier article, but a capsule summary is probably in order. Annotations were added to Python as a general feature, long before the static-typing features that use annotations came along. As originally envisioned in 2006, annotations were simply meant as a way to attach metadata to a function's arguments and return value—and to make the information available at run time in the __annotations__ dictionary associated with the function. Syntax was added so that programmers could optionally add this information to function definitions; interpretation of the metadata was left up to whatever was processing it. Type information was certainly one of the possibilities for that metadata, but there was no standard on how to represent the types of arguments or return values.
When type hints came about, the type information was naturally stored using the existing annotations mechanism, but eventually ran into some snags, with forward references to types being particularly problematic. So PEP 563 was adopted to defer the evaluation of annotations until they were actually needed; that also removed the cost of computing the annotations every time a module was imported, which was a cost that provided almost no benefit. For static type checkers, which were the main driving force behind the type hints effort, it would not change anything; those checks are not done at run time, so the values that got stored in __annotations__ were not used.
On the other hand, there are users of the type annotations at run time and there may be users of other kinds of annotations at run time as well. It seemed to come as a bit of a surprise when developers of the pydantic data-validation tool explained the problems they had using PEP 563, but other examples came to light as well. It would not be a huge surprise to discover that annotations are being used at run time for some non-type-related purpose—somewhere out there in the enormous Python ecosystem. Annotations were added to the language as a generalized feature so it is not a huge stretch to imagine that developers have used them in unexpected ways.
For some, including Python creator Guido van Rossum, restricting annotations to type information is a reasonable way forward. But that still does not solve the problems for the run-time users of __annotations__, who ran aground on some of the scoping issues that are inherent in the PEP 563 approach. But switching to PEP 563 is a one-way door; if a project like pydantic wanted the old behavior, it would not have been able to get it. That meant that even those who were using the annotations for type information could not do so, at least reasonably easily, at run time.
PEP 649 took a different approach that was meant to resolve the forward-reference problems and to maintain the performance boost that came from not evaluating the annotations until they were actually needed. Instead of keeping that annotations around in string form, as PEP 563 does, it turns them into functions that get called the first time __annotations__ is consulted. Those functions can properly handle the different local and global scopes that bedeviled the earlier approach, which effectively used eval() (or typing.get_type_hints()) to turn the annotation strings into Python objects.
Resurrection
But PEP 649 came about rather late in the development cycle for Python 3.10 and seemingly only barely headed off the switch to PEP 563 by default for that release. Instead of waiting until April 2022, just before the Python 3.11 feature freeze, as PEP 563 author Łukasz Langa jokingly warned against, Smith raised the question. He would like to see PEP 649 adopted, which was a fairly popular option back in April when it was discussed. He wondered what the next steps would be:
My understanding is that PEP 649 is currently in front of the SC [steering council]. But do we need to have any additional discussion here? My recollection is that we backed out the PEP 563 change because we didn't feel we had enough time to come to a good decision one way or the other before 3.10.
Council member Barry Warsaw filled
in some of the considerations that led the council to defer the
question, beyond just the problem of running out of time. These are
"decisions we’d have to live with essentially forever
", he
said. One
of those considerations is
the question of compile-time versus run-time use of the annotations; he
thinks that the two groups need to cooperate:
[...] we have to be very careful that the folks who use type annotation at compile/static checking time (e.g. mypy and friends) explicitly consider the existing use cases and needs of the runtime type community. These two constituents have to work together to avoid backward incompatible changes.
Luciano Ramalho noted that the large companies that have invested a lot of effort into Python typing features tend to be focused on the static-typing case and are concerned by its costs, so it is up to the rest of the community to ensure that the other use cases are still well-supported:
[...] static checking only happens in developer workstations and CI servers, but *imports* happen all the time in code running in production, on countless servers. That became a real issue for those very same companies operating at Web scale.So they have a strong incentive to focus on the use of annotations for static checking only, while many of us also want type hints to address use cases where Python is used as a *dynamic* language, which is its nature, and likely a strong reason for its popularity in the first place—despite the inherent runtime costs of being a dynamic language.
Another steering council member, Brett Cannon, confirmed
that PEP 649 was currently under consideration, but he did think that
additional discussion was needed: "I think the question is whether we
have general consensus around PEP 649?
" In addition, Inada Naoki said
that there was a need to evaluate the memory and performance impact of
PEP 649 before deciding on it.
But PEP 649 author Larry Hastings does not see things that way. As he pointed out, PEP 563 does not mention performance or memory consumption; it is focused on solving a problem in the language. PEP 649 should be treated similarly:
I think PEP 649 should be considered in the same way. In my opinion, the important thing is to figure out what semantics we want for the language. Once we figure out what semantics we want, we should implement them, and only then should we start worrying about performance. Fretting about performance at this point is premature and a distraction.I assert PEP 649's performance and memory use is already acceptable, particularly for a prototype. And I'm confident that if PEP 649 is accepted, the core dev community will find endless ways to optimize the implementation.
Finding the right balance
There is, to a certain extent, a struggle going on between those who want
to further enshrine type features into annotations and, by extension, the
Python language itself, and those who are perfectly happy to see the typing
features, but do not want them to preclude other uses of annotations.
That echoes Ramalho's observations about the companies behind the
static-typing feature somewhat. For example, PEP 646
("Variadic Generics
") proposes "syntax for
type annotations that may or may not be useful or desired for regular
Python
", as Warsaw put it. But having the syntax of the language
and that of its type
annotations diverge is not something that he believes the council will
allow.
There are aspects of PEP 649 (or some, as yet unwritten, successor) where typing proponents would like push annotation support in directions that might wall off other uses. Hastings is particularly concerned by that:
Annotations aren't special enough to break the rules.I worry about Python-the-language enshrining design choices made by the typing module. Python is now on its fourth string interpolation technology, and it ships with three command-line argument parsing libraries; in each of these cases, we were adding a New Thing that was viewed at the time as an improvement over the existing thing(s). It'd be an act of hubris to assert that the current "typing" module is the ultimate, final library for expressing type information in Python. But if we tie the language too strongly to the typing module, I fear we could strangle its successors in their cribs.
Steve Holden agreed
with that concern, noting that "optional" may be slowly getting elbowed
aside. "Which would be unfortunate given the (explicit?) assurances
that annotations would be optional; they are casting their shadow over the
whole language.
" For some, it is about finding the balance between
the needs of the new feature without precluding older uses; others,
including Van Rossum, seem more willing to fully embrace annotations only
for types and mostly only for static analysis.
One gets the feeling that this particular debate would have played out
quite a bit differently if Van Rossum were still the benevolent dictator
for the language. But he voluntarily
relinquished that role and has fully
embraced the steering council model that the community adopted. That model
purposely provides for multiple voices that can try to find the balance in
a disagreement of this sort. Van Rossum credited the council with
"the wisdom of Solomon
" in his reaction
to the deferral decision back in April, but it may well be that the council
in fact has a wisdom of a different sort entirely. Python seems likely to
benefit from its multi-headed wisdom going forward.
While the fate of PEP 649 itself is somewhat unclear at this point, it
does seem like there are efforts being made to enhance it to cover the
problem areas that have been brought up. Given the known use cases for
annotations at run time (e.g. pydantic), though, queuing up PEP 563 as
the default for 3.11 seems highly unlikely. There is still lots of
time to discuss, further prototype, and revise the idea well before hitting
Langa's "deadline". We may see it all resolve before 2021 ends, in truth—stay tuned.
| Index entries for this article | |
|---|---|
| Python | Annotations |
| Python | Python Enhancement Proposals (PEP)/PEP 649 |
