https://github.com/python/cpython/commit/702d08578394a387ad5099befe79acf6615cb27e
commit: 702d08578394a387ad5099befe79acf6615cb27e
branch: 3.13
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2026-03-02T20:03:08Z
summary:

[3.13] gh-130327: Always traverse managed dictionaries, even when inline values 
are available (GH-130469) (#145440)

Co-authored-by: Peter Bierma <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst
M Lib/test/test_dict.py
M Objects/dictobject.c

diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index 7ae764bfdfc202..0844d2bceda7a3 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -1663,6 +1663,25 @@ def make_pairs():
                 self.assertEqual(d.get(key3_3), 44)
                 self.assertGreaterEqual(eq_count, 1)
 
+    def test_overwrite_managed_dict(self):
+        # GH-130327: Overwriting an object's managed dictionary with another 
object's
+        # skipped traversal in favor of inline values, causing the GC to 
believe that
+        # the __dict__ wasn't reachable.
+        import gc
+
+        class Shenanigans:
+            pass
+
+        to_be_deleted = Shenanigans()
+        to_be_deleted.attr = "whatever"
+        holds_reference = Shenanigans()
+        holds_reference.__dict__ = to_be_deleted.__dict__
+        holds_reference.ref = {"circular": to_be_deleted, "data": 42}
+
+        del to_be_deleted
+        gc.collect()
+        self.assertEqual(holds_reference.ref['data'], 42)
+        self.assertEqual(holds_reference.attr, "whatever")
     def test_clear_at_lookup(self):
         # gh-140551 dict crash if clear is called at lookup stage
         class X:
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst
new file mode 100644
index 00000000000000..9b9a282b5ab414
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst
@@ -0,0 +1,2 @@
+Fix erroneous clearing of an object's :attr:`~object.__dict__` if
+overwritten at runtime.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 1c9bf05cbeb279..843ece535be63f 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -4557,10 +4557,8 @@ dict_traverse(PyObject *op, visitproc visit, void *arg)
 
     if (DK_IS_UNICODE(keys)) {
         if (_PyDict_HasSplitTable(mp)) {
-            if (!mp->ma_values->embedded) {
-                for (i = 0; i < n; i++) {
-                    Py_VISIT(mp->ma_values->values[i]);
-                }
+            for (i = 0; i < n; i++) {
+                Py_VISIT(mp->ma_values->values[i]);
             }
         }
         else {
@@ -7128,16 +7126,21 @@ PyObject_VisitManagedDict(PyObject *obj, visitproc 
visit, void *arg)
     if((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
         return 0;
     }
-    if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
+    PyDictObject *dict = _PyObject_ManagedDictPointer(obj)->dict;
+    if (dict != NULL) {
+        // GH-130327: If there's a managed dictionary available, we should
+        // *always* traverse it. The dict is responsible for traversing the
+        // inline values if it points to them.
+        Py_VISIT(dict);
+    }
+    else if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
         PyDictValues *values = _PyObject_InlineValues(obj);
         if (values->valid) {
             for (Py_ssize_t i = 0; i < values->capacity; i++) {
                 Py_VISIT(values->values[i]);
             }
-            return 0;
         }
     }
-    Py_VISIT(_PyObject_ManagedDictPointer(obj)->dict);
     return 0;
 }
 

_______________________________________________
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