Description
Bug report
Bug description:
When using automatically-stringified annotations (from __future__ import annotations
), typing_extensions.ClassVar
is not treated the same as typing.ClassVar
even though those two objects are identical. Specifically, the dataclasses.dataclass
-produced __init__
method does not exclude typing_extensions.ClassVar
-wrapped variables from its annotations, but does exclude typing.ClassVar
-wrapped variables from its annotations. Among other problems, this causes typing.get_type_hints
to choke.
Reproducer below:
from __future__ import annotations
import dataclasses as dcls
import inspect
import typing
import typing_extensions as typx
assert typing.ClassVar is typx.ClassVar
@dcls.dataclass
class Foo:
y: int
x: typing.ClassVar[ int ] = 1
print( "\n=== typing.ClassVar (ok) ===" )
print( 'anno', Foo.__init__.__annotations__ )
print( 'inspect', inspect.get_annotations( Foo.__init__, eval_str = True ) )
print( 'typing', typing.get_type_hints( Foo.__init__ ) )
print( 'typx', typx.get_type_hints( Foo.__init__ ) )
@dcls.dataclass
class Bar:
y: int
x: typx.ClassVar[ int ] = 1
print( "\n=== typing_extensions.ClassVar (breaks) ===" )
print( 'anno', Bar.__init__.__annotations__ )
print( 'inspect', inspect.get_annotations( Bar.__init__, eval_str = True ) )
print( 'typing', typing.get_type_hints( Bar.__init__ ) )
print( 'typx', typx.get_type_hints( Bar.__init__ ) )
This results in the following output:
=== typing.ClassVar (ok) ===
anno {'y': 'int', 'return': None}
inspect {'y': <class 'int'>, 'return': None}
typing {'y': <class 'int'>, 'return': <class 'NoneType'>}
typx {'y': <class 'int'>, 'return': <class 'NoneType'>}
=== typing_extensions.ClassVar (breaks) ===
anno {'y': 'int', 'x': 'typx.ClassVar[int]', 'return': None}
inspect {'y': <class 'int'>, 'x': typing.ClassVar[int], 'return': None}
Traceback (most recent call last):
File "/home/me/src/somepkg/bugs/dcls-classvar-hints.py", line 38, in <module>
print( 'typing', typing.get_type_hints( Bar.__init__ ) )
File "/usr/lib/python3.10/typing.py", line 1871, in get_type_hints
value = _eval_type(value, globalns, localns)
File "/usr/lib/python3.10/typing.py", line 327, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "/usr/lib/python3.10/typing.py", line 693, in _evaluate
type_ = _type_check(
File "/usr/lib/python3.10/typing.py", line 167, in _type_check
raise TypeError(f"{arg} is not valid as type argument")
TypeError: typing.ClassVar[int] is not valid as type argument
(Credit to @Daraan for helping me pinpoint the problem.)
This is reproducible on both Python 3.10 and 3.13.
Possibly related to #89687.
CPython versions tested on:
3.10, 3.13
Operating systems tested on:
No response
Activity
JelleZijlstra commentedon May 13, 2025
This isn't just about the annotations, the dataclass also generates the wrong parameters. The
Bar
class in your example is inferred as having two parameters:dataclasses does some complicated dance to identify
typing.ClassVar
in string annotations; see thedataclasses._is_type
function. That function doesn't appear to support cases wheretyping.ClassVar
is re-exported from another module. Maybe it could be fixed.My general recommendation though is to not use
from __future__ import annotations
if you want your annotations to be introspected reliably.JelleZijlstra commentedon May 13, 2025
cc @ericvsmith @carljm for dataclasses
emcd commentedon May 13, 2025
Not to distract from the topic at hand, but can you elaborate on this advice? Not using stringified forward references can impose a lot of additional positioning constraints on code. I already have to dance around the fact that the values, which are bound to
TypeAlias
variables, are not forward references. Would prefer to not to make the dance any more complicated.I have seen comments about how a change in Python 3.14+ is supposed to fix this, but that is years away from being a baseline version in terms of what needs to be supported. What is the advice in the meantime?
(For the use case which triggered the original report, I am just going to fall back to
get_annotations
and walk the MRO of classes myself to merge inherited annotations. I cannot control how users of my library may surface annotations on their objects.)JelleZijlstra commentedon May 13, 2025
Manually add strings only for specific types that need it.
emcd commentedon May 13, 2025
Fair. That is cognitive overhead during development, but... if one has a type checker's language server hooked up to their development environment, then it is probably bearable.
ericvsmith commentedon May 14, 2025
I'd prefer not to add workarounds in
_is_type
for third party packages. I think that going forward, the best approach is @JelleZijlstra ’s suggestion to not usefrom __future__ import annotations
and to use explicit strings where necessary. Maybe we should discuss this on d.p.o. and come up with a policy on it.emcd commentedon May 15, 2025
dzherb commentedon May 15, 2025
I'd like to work on this
sobolevn commentedon May 15, 2025
@dzherb thanks! You can experiment with #133956 (comment) and add tests for this case. Probably, some other corner-cases can be found :)
emcd commentedon May 15, 2025
Yes, looks good. This is what I had in mind when I wrote "if that name points to a module, inspect that module's
__dict__
..." in my previous response. I can work on this in a few days from now, but am focused on getting a project across the line currently.Thanks. Go for it. If you don't get to it in a few days, then I can pick it up.
Not sure what the policy is on back-porting fixes, but it would be nice to see this in the next patch/micro releases of 3.9 - 3.13 too.
sobolevn commentedon May 15, 2025
We can backport fixes to 3.13 and 3.14 only :(
Others are security-only at the moment.
4 remaining items