From 2160254b30ee50e8b045c44a6218b56b7c39bc2b Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 08:29:50 +0300 Subject: [PATCH 1/9] Use __class__ in setattr and delattr --- Lib/dataclasses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 730ced7299865e..51092baf27c2a5 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -726,9 +726,9 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, def _frozen_get_del_attr(cls, fields, func_builder): - locals = {'cls': cls, + locals = {'__class__': cls, 'FrozenInstanceError': FrozenInstanceError} - condition = 'type(self) is cls' + condition = 'type(self) is __class__' if fields: condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}' @@ -736,14 +736,14 @@ def _frozen_get_del_attr(cls, fields, func_builder): ('self', 'name', 'value'), (f' if {condition}:', ' raise FrozenInstanceError(f"cannot assign to field {name!r}")', - f' super(cls, self).__setattr__(name, value)'), + f' super(__class__, self).__setattr__(name, value)'), locals=locals, overwrite_error=True) func_builder.add_fn('__delattr__', ('self', 'name'), (f' if {condition}:', ' raise FrozenInstanceError(f"cannot delete field {name!r}")', - f' super(cls, self).__delattr__(name)'), + f' super(__class__, self).__delattr__(name)'), locals=locals, overwrite_error=True) From a6de29b287715f782f1b92c8c3717d1dcb1e908e Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 08:30:31 +0300 Subject: [PATCH 2/9] Rename _frozen_get_del_attr --- Lib/dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 51092baf27c2a5..e4656dd7944000 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -725,7 +725,7 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, annotation_fields=annotation_fields) -def _frozen_get_del_attr(cls, fields, func_builder): +def _frozen_set_del_attr(cls, fields, func_builder): locals = {'__class__': cls, 'FrozenInstanceError': FrozenInstanceError} condition = 'type(self) is __class__' @@ -1199,7 +1199,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, overwrite_error='Consider using functools.total_ordering') if frozen: - _frozen_get_del_attr(cls, field_list, func_builder) + _frozen_set_del_attr(cls, field_list, func_builder) # Decide if/how we're going to create a hash function. hash_action = _hash_action[bool(unsafe_hash), From 63c4e191b91d4fc348f6f5a7df03c398dd2b4167 Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 06:49:57 +0000 Subject: [PATCH 3/9] Add tests --- Lib/test/test_dataclasses/__init__.py | 34 ++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 3b335429b98500..5b779567609805 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -3971,6 +3971,13 @@ class SlotsTest: return SlotsTest + def make_frozen(): + @dataclass(frozen=True, slots=True) + class SlotsTest: + pass + + return SlotsTest + def make_with_annotations(): @dataclass(slots=True) class SlotsTest: @@ -3996,7 +4003,7 @@ class SlotsTest: return SlotsTest - for make in (make_simple, make_with_annotations, make_with_annotations_and_method, make_with_forwardref): + for make in (make_simple, make_frozen, make_with_annotations, make_with_annotations_and_method, make_with_forwardref): with self.subTest(make=make): C = make() support.gc_collect() @@ -4004,6 +4011,31 @@ class SlotsTest: and cls.__firstlineno__ == make.__code__.co_firstlineno + 1] self.assertEqual(candidates, [C]) + def test_set_del_attr_reference_new_class(self): + @dataclass(frozen=True, slots=True) + class SetDelAttrTest: + pass + + for method_name in ('__setattr__', '__delattr__'): + with self.subTest(method_name=method_name): + method = getattr(SetDelAttrTest, method_name) + cell_idx = method.__code__.co_freevars.index('__class__') + reference = method.__closure__[cell_idx].cell_contents + self.assertIs(reference, SetDelAttrTest) + + def test_set_del_attr_do_not_reference_old_class(self): + class SetDelAttrTest: + pass + + OriginalCls = SetDelAttrTest + SetDelAttrTest = dataclass(frozen=True, slots=True)(SetDelAttrTest) + + for method_name in ('__setattr__', '__delattr__'): + with self.subTest(method_name=method_name): + method = getattr(SetDelAttrTest, method_name) + cell_contents = [cell.cell_contents for cell in method.__closure__] + self.assertNotIn(OriginalCls, cell_contents) + class TestDescriptors(unittest.TestCase): def test_set_name(self): From d35771650f02e3ab21345ec5f7e8fae1b9be0ecf Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 20:50:41 +0000 Subject: [PATCH 4/9] Rename test --- 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 5b779567609805..03c820a4b72d86 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4011,7 +4011,7 @@ class SlotsTest: and cls.__firstlineno__ == make.__code__.co_firstlineno + 1] self.assertEqual(candidates, [C]) - def test_set_del_attr_reference_new_class(self): + def test_set_del_attr_reference_new_class_via__class__(self): @dataclass(frozen=True, slots=True) class SetDelAttrTest: pass From 9b33e89370ff5778b01286f78954c189eff1383f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:23:19 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst new file mode 100644 index 00000000000000..292587e76c9c42 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst @@ -0,0 +1 @@ +When both *frozen* and *slots* are set to ``True`` in :func:`~dataclasses.dataclass`, added methods ``__setattr__`` and ``__delattr__`` were still referencing the original class, preventing it from being garbage collected and making one of the conditions inside these methods to be always ``False``. From bff566a20c23df5fe45b9d7fef4d4119cbcf8673 Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 21:29:06 +0000 Subject: [PATCH 6/9] Reformat news entry --- .../Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst index 292587e76c9c42..c327771faab48c 100644 --- a/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst +++ b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst @@ -1 +1,5 @@ -When both *frozen* and *slots* are set to ``True`` in :func:`~dataclasses.dataclass`, added methods ``__setattr__`` and ``__delattr__`` were still referencing the original class, preventing it from being garbage collected and making one of the conditions inside these methods to be always ``False``. +When both *frozen* and *slots* are set to ``True`` +in :func:`~dataclasses.dataclass`, added methods +``__setattr__`` and ``__delattr__`` were still referencing the original class, +preventing it from being garbage collected and making one of the conditions +inside these methods to be always ``False``. From 883363fa62a12c615c05cfeafff4d19417559094 Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 22:05:22 +0000 Subject: [PATCH 7/9] Move tests to other case --- Lib/test/test_dataclasses/__init__.py | 53 ++++++++++++++------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 03c820a4b72d86..e9296b5301f5c1 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -3405,6 +3405,33 @@ class C: c = C('hello') self.assertEqual(deepcopy(c), c) + def test_slotted_set_del_attr_reference_new_class_via__class__(self): + # See https://github.com/python/cpython/pull/144021 + @dataclass(frozen=True, slots=True) + class SetDelAttrTest: + pass + + for method_name in ('__setattr__', '__delattr__'): + with self.subTest(method_name=method_name): + method = getattr(SetDelAttrTest, method_name) + cell_idx = method.__code__.co_freevars.index('__class__') + reference = method.__closure__[cell_idx].cell_contents + self.assertIs(reference, SetDelAttrTest) + + def test_slotted_set_del_attr_do_not_reference_old_class(self): + # See https://github.com/python/cpython/pull/144021 + class SetDelAttrTest: + pass + + OriginalCls = SetDelAttrTest + SetDelAttrTest = dataclass(frozen=True, slots=True)(SetDelAttrTest) + + for method_name in ('__setattr__', '__delattr__'): + with self.subTest(method_name=method_name): + method = getattr(SetDelAttrTest, method_name) + cell_contents = [cell.cell_contents for cell in method.__closure__] + self.assertNotIn(OriginalCls, cell_contents) + class TestSlots(unittest.TestCase): def test_simple(self): @@ -3971,6 +3998,7 @@ class SlotsTest: return SlotsTest + # See https://github.com/python/cpython/issues/135228#issuecomment-3755979059 def make_frozen(): @dataclass(frozen=True, slots=True) class SlotsTest: @@ -4011,31 +4039,6 @@ class SlotsTest: and cls.__firstlineno__ == make.__code__.co_firstlineno + 1] self.assertEqual(candidates, [C]) - def test_set_del_attr_reference_new_class_via__class__(self): - @dataclass(frozen=True, slots=True) - class SetDelAttrTest: - pass - - for method_name in ('__setattr__', '__delattr__'): - with self.subTest(method_name=method_name): - method = getattr(SetDelAttrTest, method_name) - cell_idx = method.__code__.co_freevars.index('__class__') - reference = method.__closure__[cell_idx].cell_contents - self.assertIs(reference, SetDelAttrTest) - - def test_set_del_attr_do_not_reference_old_class(self): - class SetDelAttrTest: - pass - - OriginalCls = SetDelAttrTest - SetDelAttrTest = dataclass(frozen=True, slots=True)(SetDelAttrTest) - - for method_name in ('__setattr__', '__delattr__'): - with self.subTest(method_name=method_name): - method = getattr(SetDelAttrTest, method_name) - cell_contents = [cell.cell_contents for cell in method.__closure__] - self.assertNotIn(OriginalCls, cell_contents) - class TestDescriptors(unittest.TestCase): def test_set_name(self): From 1c8f7b92c6f376146b34836fb54dac356512c41d Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 22:06:28 +0000 Subject: [PATCH 8/9] Add another test --- Lib/test/test_dataclasses/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index e9296b5301f5c1..c93b7965e16c61 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -3076,6 +3076,20 @@ class C: with self.assertRaises(FrozenInstanceError): del c.i + def test_frozen_slotted(self): + # See https://github.com/python/cpython/pull/144021 + @dataclass(frozen=True, slots=True) + class C: + pass + + c = C() + # Mutating not defined fields must raise FrozenInstanceError. + with self.assertRaises(FrozenInstanceError): + c.any_field = 5 + + with self.assertRaises(FrozenInstanceError): + del c.any_field + def test_inherit(self): @dataclass(frozen=True) class C: From bb82978857fc2eda07729285b40b95927a01aa18 Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 22:14:35 +0000 Subject: [PATCH 9/9] Rephrase news entry --- .../Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst index c327771faab48c..26a1061572a71e 100644 --- a/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst +++ b/Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-135228.dGrzjM.rst @@ -1,5 +1,4 @@ When both *frozen* and *slots* are set to ``True`` in :func:`~dataclasses.dataclass`, added methods ``__setattr__`` and ``__delattr__`` were still referencing the original class, -preventing it from being garbage collected and making one of the conditions -inside these methods to be always ``False``. +preventing it from being garbage collected and causing unexpected exceptions.