From 2160254b30ee50e8b045c44a6218b56b7c39bc2b Mon Sep 17 00:00:00 2001 From: Prometheus3375 Date: Mon, 19 Jan 2026 08:29:50 +0300 Subject: [PATCH 1/3] 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/3] 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/3] 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):