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:
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:
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
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.)
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/SZA3BUYWYVYS45PZT5NS4TOEZH6SLZIP/
Code of Conduct: http://python.org/psf/codeofconduct/
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.
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 -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Hi Larry, all, I was thinking also of a compromise but a slightly different approach:
Store annotations as a subclass of string but with the required frames attached to evaluate them as though they were in their local context. Then have a function "get_annotation_values" that knows how to evaluate these string subclasses with the attached frames.
This would allow those who use runtime annotations to access local scope like PEP 649, and allow those who use static type checking to relax the syntax (as long as they don't try and evaluate the syntax at runtime) as per PEP 563.
Something akin to this was proposed and discarded during the discussion of PEP 563, although the idea there was to still use actual Python bytecode instead of strings:
It was rejected because it would be too expensive in terms of resources. PEP 649's approach uses significantly fewer resources, which is one of the reasons it seems viable.
Also, I don't see the benefit of requiring a function like "get_annotation_values" to see the actual values. This would force library code that examined annotations to change; I think it's better that we preserve the behavior that "o.__annotations__" are real values.
Cheers,
/arry
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)
I genuinely don't understand what you're proposing. Could you
elaborate?
I will note however that your example adds a lot of instances of
quoting and curly braces and the letter 'f'. Part of the reason
that PEP 563 exists is that users of type hints didn't like
quoting them all the time. Also, explicitly putting quotes around
type hints means that Python didn't examine them at compile-time,
so outright syntax errors would not be caught at compile-time.
PEP 563 meant that syntax errors would be caught at compile-time.
(Though PEP 563 still delays other errors, like NameError and
ValueError, until runtime, the same way that PEP 649 does.)
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.
__future__.unicode_literals changed the default behavior of
strings so that they became Unicode. An important part of my
proposal is that it minimizes the observable change in behavior at
runtime. PEP 563 changes "o.__annotations__" so that it contains
stringized annotations, my proposal changes that so it returns
real values, assuming eval() succeeds.
What if the eval() fails, with a NameLookup or whatever? Yes,
this would change observable behavior. Without the compile-time
flag enabled, the annotation fails to evaluate correctly at import
time. With the compile-time flag enabled, the annotation fails to
evaluate correctly at the time it's examined. I think this is
generally a feature anyway. As you observe in the next paragraph,
the vast majority of annotations are unused at runtime. If a
program didn't need an annotation at runtime, then making it
succeed at import time for something it doesn't care about seems
like a reasonable change in behavior. The downside is, nested
library code might make it hard to determine which object had the
bad annotation, though perhaps we can avoid this by crafting a
better error message for the exception.
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.
The conversion is lazy. If the annotation is never examined at runtime, it's left in the state the compiler defined it in. Where does it waste cycles?
Cheers,
/arry
Java deals with this problem by making its annotations compile-time only unless you mark them to be kept at runtime)
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.
"o.__str_annotations__"
On 4/18/21 9:14 AM, Richard Levasseur wrote: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.
I genuinely don't understand what you're proposing. Could you elaborate?
I will note however that your example adds a lot of instances of quoting and curly braces and the letter 'f'. Part of the reason that PEP 563 exists is that users of type hints didn't like quoting them all the time.
PEP 563 meant that syntax errors would be caught at compile-time.
On Sun, Apr 18, 2021 at 10:12 AM Larry Hastings <la...@hastings.org> wrote:On 4/18/21 9:14 AM, Richard Levasseur wrote: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.I genuinely don't understand what you're proposing. Could you elaborate?
I can't speak for Richard, but Interpreted this as:Have a way to specify, when you write the annotation, whether you want it evaluated or kept as a string.
in my previous post, I added the idea of the semantics (am I using that work right?) as meaning "run-time" vs "non-run time (type check time)" -- that is, do you want this to be a valid value that can be used at run time? But it could only mean "stringify or not".
I will note however that your example adds a lot of instances of quoting and curly braces and the letter 'f'. Part of the reason that PEP 563 exists is that users of type hints didn't like quoting them all the time.
I think Richard suggested the f-string because it's currently legal syntax. And you'd get syntax checking for anything in the brackets.
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.)
I thought about it some, and I think PEP 649 would still be a
good idea, even if this "PEP 1212" proposal (or a variant of it)
was workable and got accepted. PEP 649 solves the forward
references problem for most users without the restrictions of PEP
563 (or "PEP 1212"). So most people wouldn't need to turn on the
"PEP 1212" behavior.
Cheers,
/arry
Hi Larry,
This is a creative option, but I am optimistic (now that the SC
decision has removed the 3.10 deadline urgency) that we can find a
path forward that is workable for everyone and doesn't require a
permanent compiler feature flag and a language that is permanently
split-brained about annotation semantics. Since I have access to a
real-world large codebase with almost complete adoption of type
annotations (and I care about its import performance), I'm willing to
test PEP 649 on it (can't commit to doing it right away, but within
the next month or so) and see how much import performance is impacted,
and how much of that can be gained back by interning tweaks as
discussed in the other thread.
My feeling is that if the performance
turns out to be reasonably close in a real codebase, and we can find a
workable solution for `if TYPE_CHECKING`, we should go ahead with PEP
649: IMO aside from those two issues its semantics are a better fit
for the rest of the language and preferable to PEP 563.
I do think that a solution to the `if TYPE_CHECKING` problem should be
identified as part of PEP 649. My favorite option there would be a new
form of import that is lazy (the import does not actually occur until
the imported name is loaded at runtime). This has prior art in
previous discussions about "module descriptors"; IIRC Neil Schemenauer
even had a branch a while ago where all module attributes were
modified to behave this way (I might be remembering the details
wrong.)
It also has overlap with use cases served by the existing
`demandimport` library used by hg, and `importlib.util.LazyLoader`,
although it is strictly more capable because it can work with `from
module import Thing` style imports as well. If there's any interest in
this as a solution to inter-module annotation forward references, I'm
also willing to work on that in the 3.11 timeframe.