From 9d89c56db560b195230bbef93bb3f0469133d4c1 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 9 Feb 2026 05:25:24 +0100 Subject: [PATCH 1/6] Don't track `ClassVar` dataclass members as defaults --- Lib/dataclasses.py | 32 +++++++++++++------ Lib/test/test_dataclasses/__init__.py | 19 +++++++++++ ...-02-09-05-49-09.gh-issue-144618.raQvMb.rst | 3 ++ 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-09-05-49-09.gh-issue-144618.raQvMb.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 730ced7299865e..15a599c6398107 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 + + # Regular 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..e6f60d30bd6d4a 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1496,6 +1496,25 @@ class D(C): d = D(4, 5) self.assertEqual((d.x, d.z), (4, 5)) + def test_classvar_default_value_failing_descriptor(self): + class Kaboom: + def __get__(self, inst, owner): + raise RuntimeError("kaboom!") + + @dataclass + class C: + kaboom: ClassVar[None] = Kaboom() + + self.assertIsInstance(C.__dict__["kaboom"], Kaboom) + + def test_classvar_member_isnt_tracked_or_removed(self): + @dataclass + class C: + x: ClassVar[int] = 1000 + + self.assertEqual(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. From 594a8a3d0588d59bcfd4f746eba74e2ff56d7169 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 9 Feb 2026 05:56:04 +0100 Subject: [PATCH 2/6] Add references to the issue --- Lib/test/test_dataclasses/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index e6f60d30bd6d4a..4f63c9aa9d2c43 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1497,6 +1497,7 @@ class D(C): 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!") @@ -1508,6 +1509,7 @@ class C: 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 From 3530b8b4a18ecc728b7a2afaa3341512aac4cd28 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 9 Feb 2026 05:59:10 +0100 Subject: [PATCH 3/6] Fix comment ('Regular fields' is unclear) --- Lib/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 15a599c6398107..6f8b8e22e9d76c 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1085,7 +1085,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, setattr(cls, f.name, f.default) continue - # Regular fields can be set or removed as necessary. + # 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. From 9bb547002e519a546b9fa9b059cd6419fa47502a Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 9 Feb 2026 06:26:33 +0100 Subject: [PATCH 4/6] Use `assertIs` for sentinel testing --- Lib/test/test_dataclasses/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 4f63c9aa9d2c43..03e688441d57dc 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1514,7 +1514,7 @@ def test_classvar_member_isnt_tracked_or_removed(self): class C: x: ClassVar[int] = 1000 - self.assertEqual(C.__dataclass_fields__['x'].default, MISSING) + self.assertIs(C.__dataclass_fields__['x'].default, MISSING) self.assertEqual(C.x, 1000) def test_classvar_default_factory(self): From eb0611f33abc8a261b13c79ac720ff6c0a2f419f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 9 Feb 2026 06:27:57 +0100 Subject: [PATCH 5/6] Empty commit to unstuck GitHub From c3ceb58647dd21028d24a7ac889b9ad46fbd7d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 9 Feb 2026 11:46:37 +0100 Subject: [PATCH 6/6] Fix the `Kaboom` class variable annotation --- Lib/test/test_dataclasses/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 03e688441d57dc..70249553c6de2c 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1504,7 +1504,7 @@ def __get__(self, inst, owner): @dataclass class C: - kaboom: ClassVar[None] = Kaboom() + kaboom: ClassVar[Kaboom] = Kaboom() self.assertIsInstance(C.__dict__["kaboom"], Kaboom)