https://github.com/python/cpython/commit/6859b95cfff26c9ef52c5535d2b3662f17ff5b3d
commit: 6859b95cfff26c9ef52c5535d2b3662f17ff5b3d
branch: main
author: Jelle Zijlstra <[email protected]>
committer: encukou <[email protected]>
date: 2025-08-12T13:16:54+02:00
summary:

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

Remove the `__dict__` and `__weakref__` descriptors from the original class 
when creating a dataclass from it.

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.


Co-authored-by: Alyssa Coghlan <[email protected]>
Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: Serhiy Storchaka <[email protected]>

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
M Python/clinic/sysmodule.c.h
M Python/sysmodule.c

diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 83ea623dce6281..d29f1615f276d2 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -1283,6 +1283,10 @@ def _add_slots(cls, is_frozen, weakref_slot, 
defined_fields):
     if '__slots__' in cls.__dict__:
         raise TypeError(f'{cls.__name__} already specifies __slots__')
 
+    # gh-102069: Remove existing __weakref__ descriptor.
+    # gh-135228: Make sure the original class can be garbage collected.
+    sys._clear_type_descriptors(cls)
+
     # Create a new dict for our new class.
     cls_dict = dict(cls.__dict__)
     field_names = tuple(f.name for f in fields(cls))
@@ -1300,12 +1304,6 @@ def _add_slots(cls, is_frozen, weakref_slot, 
defined_fields):
         #  available in _MARKER.
         cls_dict.pop(field_name, None)
 
-    # Remove __dict__ itself.
-    cls_dict.pop('__dict__', None)
-
-    # Clear existing `__weakref__` descriptor, it belongs to a previous type:
-    cls_dict.pop('__weakref__', None)  # gh-102069
-
     # And finally create the class.
     qualname = getattr(cls, '__qualname__', None)
     newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
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..517a37feb37ae6
--- /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 can now be garbage collected again. Earlier changes in Python
+3.14 caused this class to always remain in existence together with the 
replacement
+class synthesized by :mod:`dataclasses`.
diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h
index a47e4d11b54441..09ce77fd12608f 100644
--- a/Python/clinic/sysmodule.c.h
+++ b/Python/clinic/sysmodule.c.h
@@ -1793,6 +1793,37 @@ sys__baserepl(PyObject *module, PyObject 
*Py_UNUSED(ignored))
     return sys__baserepl_impl(module);
 }
 
+PyDoc_STRVAR(sys__clear_type_descriptors__doc__,
+"_clear_type_descriptors($module, type, /)\n"
+"--\n"
+"\n"
+"Private function for clearing certain descriptors from a type\'s 
dictionary.\n"
+"\n"
+"See gh-135228 for context.");
+
+#define SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF    \
+    {"_clear_type_descriptors", (PyCFunction)sys__clear_type_descriptors, 
METH_O, sys__clear_type_descriptors__doc__},
+
+static PyObject *
+sys__clear_type_descriptors_impl(PyObject *module, PyObject *type);
+
+static PyObject *
+sys__clear_type_descriptors(PyObject *module, PyObject *arg)
+{
+    PyObject *return_value = NULL;
+    PyObject *type;
+
+    if (!PyObject_TypeCheck(arg, &PyType_Type)) {
+        _PyArg_BadArgument("_clear_type_descriptors", "argument", 
(&PyType_Type)->tp_name, arg);
+        goto exit;
+    }
+    type = arg;
+    return_value = sys__clear_type_descriptors_impl(module, type);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(sys__is_gil_enabled__doc__,
 "_is_gil_enabled($module, /)\n"
 "--\n"
@@ -1948,4 +1979,4 @@ _jit_is_active(PyObject *module, PyObject 
*Py_UNUSED(ignored))
 #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
     #define SYS_GETANDROIDAPILEVEL_METHODDEF
 #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
-/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=9052f399f40ca32d input=a9049054013a1b77]*/
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index ae6cf306735939..e4bc27d2ce624c 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -2641,6 +2641,47 @@ sys__baserepl_impl(PyObject *module)
     Py_RETURN_NONE;
 }
 
+/*[clinic input]
+sys._clear_type_descriptors
+
+    type: object(subclass_of='&PyType_Type')
+    /
+
+Private function for clearing certain descriptors from a type's dictionary.
+
+See gh-135228 for context.
+[clinic start generated code]*/
+
+static PyObject *
+sys__clear_type_descriptors_impl(PyObject *module, PyObject *type)
+/*[clinic end generated code: output=5ad17851b762b6d9 input=dc536c97fde07251]*/
+{
+    PyTypeObject *typeobj = (PyTypeObject *)type;
+    if (_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) {
+        PyErr_SetString(PyExc_TypeError, "argument is immutable");
+        return NULL;
+    }
+    PyObject *dict = _PyType_GetDict(typeobj);
+    PyObject *dunder_dict = NULL;
+    if (PyDict_Pop(dict, &_Py_ID(__dict__), &dunder_dict) < 0) {
+        return NULL;
+    }
+    PyObject *dunder_weakref = NULL;
+    if (PyDict_Pop(dict, &_Py_ID(__weakref__), &dunder_weakref) < 0) {
+        PyType_Modified(typeobj);
+        Py_XDECREF(dunder_dict);
+        return NULL;
+    }
+    PyType_Modified(typeobj);
+    // We try to hold onto a reference to these until after we call
+    // PyType_Modified(), in case their deallocation triggers somer user code
+    // that tries to do something to the type.
+    Py_XDECREF(dunder_dict);
+    Py_XDECREF(dunder_weakref);
+    Py_RETURN_NONE;
+}
+
+
 /*[clinic input]
 sys._is_gil_enabled -> bool
 
@@ -2837,6 +2878,7 @@ static PyMethodDef sys_methods[] = {
     SYS__STATS_DUMP_METHODDEF
 #endif
     SYS__GET_CPU_COUNT_CONFIG_METHODDEF
+    SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF
     SYS__IS_GIL_ENABLED_METHODDEF
     SYS__DUMP_TRACELETS_METHODDEF
     {NULL, NULL}  // sentinel

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to