https://github.com/python/cpython/commit/592e8f0865cfd5af65fbcdbd185c3dd576c838e7
commit: 592e8f0865cfd5af65fbcdbd185c3dd576c838e7
branch: main
author: Peter Bierma <[email protected]>
committer: colesbury <[email protected]>
date: 2026-03-02T14:32:06-05:00
summary:
gh-130327: Always traverse managed dictionaries, even when inline values are
available (#130469)
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 f28155fe50148d..162b0b38f8555d 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -1569,6 +1569,26 @@ 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_unhashable_key(self):
d = {'a': 1}
key = [1, 2, 3]
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 bcd3c862fd59b2..5894fdb614ebdc 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -4825,10 +4825,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 {
@@ -7413,16 +7411,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]