Skip to content

dataclasses: Synthetic __init__ method on dataclass has broken ClassVar annotation under some conditions. #133956

Open
@emcd

Description

@emcd

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

Linked PRs

Activity

added
type-bugAn unexpected behavior, bug, or error
on May 12, 2025
JelleZijlstra

JelleZijlstra commented on May 13, 2025

@JelleZijlstra
Member

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:

>>> Bar(1, 2)
Bar(y=1, x=2)

dataclasses does some complicated dance to identify typing.ClassVar in string annotations; see the dataclasses._is_type function. That function doesn't appear to support cases where typing.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

JelleZijlstra commented on May 13, 2025

@JelleZijlstra
Member

cc @ericvsmith @carljm for dataclasses

emcd

emcd commented on May 13, 2025

@emcd
Author

My general recommendation though is to not use from __future__ import annotations if you want your annotations to be introspected reliably.

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

JelleZijlstra commented on May 13, 2025

@JelleZijlstra
Member

can you elaborate on this advice? Not using stringified forward references can impose a lot of additional positioning constraints on code.

Manually add strings only for specific types that need it.

emcd

emcd commented on May 13, 2025

@emcd
Author

Manually add strings only for specific types that need it.

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

ericvsmith commented on May 14, 2025

@ericvsmith
Member

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 use from __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

emcd commented on May 15, 2025

@emcd
MemberAuthor
No description provided.
dzherb

dzherb commented on May 15, 2025

@dzherb

I'd like to work on this

sobolevn

sobolevn commented on May 15, 2025

@sobolevn
Member

@dzherb thanks! You can experiment with #133956 (comment) and add tests for this case. Probably, some other corner-cases can be found :)

emcd

emcd commented on May 15, 2025

@emcd
Author

I think the particular issue here is probably fixable, something like this:

-            if module and module.__dict__.get(module_name) is a_module:
-                ns = sys.modules.get(a_type.__module__).__dict__
+            if module and isinstance(module.__dict__.get(module_name), types.ModuleType):
+                ns = module.__dict__.get(module_name).__dict__

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.

I'd like to work on this

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

sobolevn commented on May 15, 2025

@sobolevn
Member

We can backport fixes to 3.13 and 3.14 only :(
Others are security-only at the moment.

4 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibPython modules in the Lib dirtopic-dataclassestype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      `dataclasses`: Synthetic `__init__` method on dataclass has broken `ClassVar` annotation under some conditions. · Issue #133956 · python/cpython

      Follow Lee on X/Twitter - Father, Husband, Serial builder creating AI, crypto, games & web tools. We are friends :) AI Will Come To Life!

      Check out: eBank.nz (Art Generator) | Netwrck.com (AI Tools) | Text-Generator.io (AI API) | BitBank.nz (Crypto AI) | ReadingTime (Kids Reading) | RewordGame | BigMultiplayerChess | WebFiddle | How.nz | Helix AI Assistant