
On Sat, Apr 17, 2021 at 8:46 PM Larry Hastings <larry@hastings.org> wrote:
The heart of the debate between PEPs 563 and 649 is the question: what should an annotation be? Should it be a string or a Python value? It seems people who are pro-PEP 563 want it to be a string, and people who are pro-PEP 649 want it to be a value.
Actually, let me amend that slightly. Most people who are pro-PEP 563 don't actually care that annotations are strings, per se. What they want are specific runtime behaviors, and they get those behaviors when PEP 563 turns their annotations into strings.
I have an idea--a rough proposal--on how we can mix together aspects of PEP 563 and PEP 649. I think it satisfies everyone's use cases for both PEPs. The behavior it gets us:
- annotations can be stored as strings - annotations stored as strings can be examined as strings - annotations can be examined as values
The idea:
We add a new type of compile-time flag, akin to a "from __future__" import, but not from the future. Let's not call it "from __present__", for now how about "from __behavior__".
In this specific case, we call it "from __behavior__ import str_annotations". It behaves much like Python 3.9 does when you say "from __future__ import annotations", except: it stores the dictionary with stringized values in a new member on the function/class/module called "__str_annotations__".
If an object "o" has "__str_annotations__", set, you can access it and see the stringized values.
If you access "o.__annotations__", and the object has "o.__str_annotations__" set but "o.__annotations__" is not set, it builds (and caches) a new dict by iterating over o.__str_annotations__, calling eval() on each value in "o.__str_annotations__". It gets the globals() dict the same way that PEP 649 does (including, if you compile a class with str_annotations, it sets __globals__ on the class). It does *not* unset "o.__str_annotations__" unless someone explicitly sets "o.__annotations__". This is so you can write your code assuming that "o.__str_annotations__" is set, and it doesn't explode if somebody somewhere ever looks at "o.__annotations__". (This could lead to them getting out of sync, if someone modified "o.__annotations__". But I suspect practicality beats purity here.)
This means:
- People who only want stringized annotations can turn it on, and only ever examine "o.__str_annotations__". They get the benefits of PEP 563: annotations don't have to be valid Python values at runtime, just parseable. They can continue doing the "if TYPE_CHECKING:" import thing. - Library code which wants to examine values can examine "o.__annotations__". We might consider amending library functions that look at annotations to add a keyword-only parameter, "str_annotations=False", and if it's true it uses o.__str_annotations__ instead etc etc etc.
I think this goes in the right direction.
Alternatively: what if the "trigger" to resolve the expression to an object was moved from a module-level setting to the specific expression? e.g. def foo(x: f'{list[int]}') -> f'{str}': bar: f'{tuple[int]}' = () @pydantic_or_whatever_that_needs_objects_from_annotations class Foo: blah: f'{tuple[int]}' = () I picked f-strings above since they're compatible with existing syntax and visible to the AST iirc; the point is some syntax/marker at the annotation level to indicate "eagerly resolve this / keep the value at runtime". Maybe "as", or ":@", or a "magic" @typing.runtime_annotations decorator, or some other bikeshed etc. (As an aside, Java deals with this problem by making its annotations compile-time only unless you mark them to be kept at runtime) The reasons I suggest this are: 1. A module-level switch reminds me of __future__.unicode_literals. Switching that on/off was a bit of a headache due to the action at a distance. 2. It's my belief that the *vast *majority of annotations are unused at runtime, so all the extra effort in resolving an annotation expression is just wasted cycles. It makes sense for the default behavior to be "string annotations", with runtime-evaluation/retention enabled when needed. 3. That said, there are definitely cases where having the resolved objects at runtime is very useful, and that should be enabled/allowed in a more "first-class" way. I think there's other benefits (when/how an error is reported, the explicitness, how that explicitness informs other tools, easier for libraries to use, avoids keeping closures around just in case, etc), but those three are the most significant IMHO. Also, yes, of course we can keep the optimization where stringized
annotations are stored as a tuple containing an even number of strings. Similarly to PEP 649's automatic binding of an unbound code object, if you set "o.__str_annotations__" to a tuple containing an even number of strings, and you then access "o.__str_annotations__", you get back a dict.
TBD: how this interacts with PEP 649. I don't know if it means we only do this, or if it would be a good idea to do both this and 649. I just haven't thought about it. (It would be a runtime error to set both "o.__str_annotations__" and "o.__co_annotations__", though.)
Well, whaddya think? Any good?
I considered calling this "PEP 1212", which is 563 + 649,
*/arry* _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/WUZGTGE4... Code of Conduct: http://python.org/psf/codeofconduct/