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