diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 730ced7299865e..6f8b8e22e9d76c 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -828,16 +828,11 @@ def _get_field(cls, a_name, a_type, default_kw_only): # default_kw_only is the value of kw_only to use if there isn't a field() # that defines it. - # If the default value isn't derived from Field, then it's only a - # normal default value. Convert it to a Field(). - default = getattr(cls, a_name, MISSING) - if isinstance(default, Field): - f = default + member = vars(cls).get(a_name, MISSING) + if isinstance(member, Field): + f = member else: - if isinstance(default, types.MemberDescriptorType): - # This is a field in __slots__, so it has no default value. - default = MISSING - f = field(default=default) + f = field() # Only at this point do we know the name and the type. Set them. f.name = a_name @@ -899,6 +894,17 @@ def _get_field(cls, a_name, a_type, default_kw_only): # kw_only validation and assignment. if f._field_type in (_FIELD, _FIELD_INITVAR): + # If the default value isn't derived from Field, then it's only a + # normal default value. + default = getattr(cls, a_name, MISSING) + + # This is a field in __slots__, so it has no default value. + if isinstance(default, types.MemberDescriptorType): + default = MISSING + + if not isinstance(default, Field): + f.default = default + # For real and InitVar fields, if kw_only wasn't specified use the # default value. if f.kw_only is MISSING: @@ -1072,6 +1078,14 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # field) exists and is of type 'Field', replace it with the # real default. This is so that normal class introspection # sees a real default value, not a Field. + + # Class variables cannot be removed from the class. + if f._field_type is _FIELD_CLASSVAR: + if f.default is not MISSING: + setattr(cls, f.name, f.default) + continue + + # Other fields can be set or removed as necessary. if isinstance(getattr(cls, f.name, None), Field): if f.default is MISSING: # If there's no default, delete the class attribute. diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 3b335429b98500..70249553c6de2c 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1496,6 +1496,27 @@ class D(C): d = D(4, 5) self.assertEqual((d.x, d.z), (4, 5)) + def test_classvar_default_value_failing_descriptor(self): + """Regression test for GH-144618.""" + class Kaboom: + def __get__(self, inst, owner): + raise RuntimeError("kaboom!") + + @dataclass + class C: + kaboom: ClassVar[Kaboom] = Kaboom() + + self.assertIsInstance(C.__dict__["kaboom"], Kaboom) + + def test_classvar_member_isnt_tracked_or_removed(self): + """Regression test for GH-144618.""" + @dataclass + class C: + x: ClassVar[int] = 1000 + + self.assertIs(C.__dataclass_fields__['x'].default, MISSING) + self.assertEqual(C.x, 1000) + def test_classvar_default_factory(self): # It's an error for a ClassVar to have a factory function. with self.assertRaisesRegex(TypeError, diff --git a/Misc/NEWS.d/next/Library/2026-02-09-05-49-09.gh-issue-144618.raQvMb.rst b/Misc/NEWS.d/next/Library/2026-02-09-05-49-09.gh-issue-144618.raQvMb.rst new file mode 100644 index 00000000000000..9e0e1180c71ce5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-09-05-49-09.gh-issue-144618.raQvMb.rst @@ -0,0 +1,3 @@ +:deco:`dataclasses.dataclass` no longer triggers ``__get__`` of +:data:`~typing.ClassVar` members nor tracks them as field defaults. Patch by +Bartosz Sławecki.