I thought Python 3.14 and its deferred annotations would let us avoid forward refs when inheriting from a generic type, but it looks like it’s not the case, and I can’t find any relevant information in PEP 649:
# generic_base.py
class Foo[T]:
...
# Works fine for a type annotation.
foobar: Foo[Bar] = Foo()
# But not when inheriting from the class.
class FooBar(Foo[Bar]):
...
class Bar:
...
% python3.14 generic_base.py
Traceback (most recent call last):
File "/home/pawamoy/generic_base.py", line 8, in <module>
class FooBar(Foo[Bar]):
^^^
NameError: name 'Bar' is not defined
% python3.14 -V
Python 3.14.0a2
Looking at the code, it seems obvious, because in the base class case, Foo[Bar]
happens at runtime. But the [Bar]
part really is typing information, so should it really affect runtime?
Lets take a look at dataclass-like transforms, for example. With new deferred annotations, these libraries will be expected to try and use inspect.get_annotations
with various formats: Format.STRING
, Format.FORWARDREF
and Format.VALUE
. The last one will fail with name errors.
from inspect import get_annotations, Format
class Foo[T]:
...
class A:
foobar: Foo[Bar] = Foo()
print(get_annotations(A, format=Format.STRING))
print(get_annotations(A, format=Format.FORWARDREF))
print(get_annotations(A, format=Format.VALUE))
class Bar:
...
% python3.14 generic_base.py
{'foobar': 'Foo[Bar]'}
{'foobar': __main__.Foo[ForwardRef('Bar')]}
Traceback (most recent call last):
File "/home/pawamoy/generic_base.py", line 12, in <module>
print(get_annotations(A, format=Format.VALUE))
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.14.0a2/lib/python3.14/annotationlib.py", line 704, in get_annotations
ann = _get_dunder_annotations(obj)
File "/home/pawamoy/.basher-packages/pyenv/pyenv/versions/3.14.0a2/lib/python3.14/annotationlib.py", line 837, in _get_dunder_annotations
ann = _BASE_GET_ANNOTATIONS(obj)
File "/home/pawamoy/generic_base.py", line 8, in __annotate__
foobar: Foo[Bar] = Foo()
^^^
NameError: name 'Bar' is not defined
It seems unfair to me that class FooBar(Foo[Bar])
triggers the same error as if inspect.get_annotations(..., format=Format.VALUE)
had been used. It feels… too early? If the [Bar]
part in inheriting from Foo[Bar]
is only meant for type-checking and dataclass-like transforms, why not give dataclass-like libraries a chance to handle this like they handle attributes? Worst case, Bar
must indeed be declared before the transform is applied, that’s a user error and the user can change its code. Best case, Bar
is not needed at all to create the FooBar
class. It’s only needed later when type-checking things.
Is it simply the price to pay for using the subscript syntax? The following should definitely be evaluated directly and not deferred, and I suppose the interpreter cannot know the difference?
class Bar:
...
classes = {"bar": Bar}
var = "bar"
class Foo(classes[var]):
...