https://github.com/python/cpython/commit/6e1b31b87e7a42c7911b517b78fc418217e6480c
commit: 6e1b31b87e7a42c7911b517b78fc418217e6480c
branch: 3.14
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-07-22T05:08:15Z
summary:

[3.14] gh-135228: When @dataclass(slots=True) replaces a dataclass, make the 
original class collectible (GH-136893) (#136960)

gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original 
class collectible (GH-136893)

An interesting hack, but more localized in scope than GH-135230.

This may be a breaking change if people intentionally keep the original class 
around
when using `@dataclass(slots=True)`, and then use `__dict__` or `__weakref__` 
on the
original class.
(cherry picked from commit 46cbdf967ada11b0286060488b61635fd6a2bb23)

Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com>
Co-authored-by: Alyssa Coghlan <ncogh...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses/__init__.py

diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 83ea623dce6281..22b78bb2fbe6ed 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -1338,6 +1338,13 @@ def _add_slots(cls, is_frozen, weakref_slot, 
defined_fields):
                 or _update_func_cell_for__class__(member.fdel, cls, newcls)):
                 break
 
+    # gh-135228: Make sure the original class can be garbage collected.
+    # Bypass mapping proxy to allow __dict__ to be removed
+    old_cls_dict = cls.__dict__ | _deproxier
+    old_cls_dict.pop('__dict__', None)
+    if "__weakref__" in cls.__dict__:
+        del cls.__weakref__
+
     return newcls
 
 
@@ -1732,3 +1739,11 @@ def _replace(self, /, **changes):
     # changes that aren't fields, this will correctly raise a
     # TypeError.
     return self.__class__(**changes)
+
+
+# Hack to the get the underlying dict out of a mappingproxy
+# Use it with: cls.__dict__ | _deproxier
+class _Deproxier:
+    def __ror__(self, other):
+        return other
+_deproxier = _Deproxier()
diff --git a/Lib/test/test_dataclasses/__init__.py 
b/Lib/test/test_dataclasses/__init__.py
index e98a8f284cec9f..6bf5e5b3e5554b 100644
--- a/Lib/test/test_dataclasses/__init__.py
+++ b/Lib/test/test_dataclasses/__init__.py
@@ -3804,6 +3804,41 @@ class WithCorrectSuper(CorrectSuper):
         # that we create internally.
         self.assertEqual(CorrectSuper.args, ["default", "default"])
 
+    def test_original_class_is_gced(self):
+        # gh-135228: Make sure when we replace the class with slots=True, the 
original class
+        # gets garbage collected.
+        def make_simple():
+            @dataclass(slots=True)
+            class SlotsTest:
+                pass
+
+            return SlotsTest
+
+        def make_with_annotations():
+            @dataclass(slots=True)
+            class SlotsTest:
+                x: int
+
+            return SlotsTest
+
+        def make_with_annotations_and_method():
+            @dataclass(slots=True)
+            class SlotsTest:
+                x: int
+
+                def method(self) -> int:
+                    return self.x
+
+            return SlotsTest
+
+        for make in (make_simple, make_with_annotations, 
make_with_annotations_and_method):
+            with self.subTest(make=make):
+                C = make()
+                support.gc_collect()
+                candidates = [cls for cls in object.__subclasses__() if 
cls.__name__ == 'SlotsTest'
+                              and cls.__firstlineno__ == 
make.__code__.co_firstlineno + 1]
+                self.assertEqual(candidates, [C])
+
 
 class TestDescriptors(unittest.TestCase):
     def test_set_name(self):
diff --git 
a/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst 
b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst
new file mode 100644
index 00000000000000..ee8962c6f46e75
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst
@@ -0,0 +1,4 @@
+When :mod:`dataclasses` replaces a class with a slotted dataclass, the
+original class is now garbage collected again. Earlier changes in Python
+3.14 caused this class to remain in existence together with the replacement
+class synthesized by :mod:`dataclasses`.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to