https://github.com/python/cpython/commit/f0562124927ec0087a5d3460efe08567aab64b84
commit: f0562124927ec0087a5d3460efe08567aab64b84
branch: main
author: Neil Schemenauer <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-13T17:45:02+05:30
summary:
gh-155515: Use GC tracking for HAMT iterators (#155517)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst
M Lib/test/test_context.py
M Python/hamt.c
diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py
index ef20495dcc01ea..f193a333521059 100644
--- a/Lib/test/test_context.py
+++ b/Lib/test/test_context.py
@@ -1282,6 +1282,31 @@ def test_hamt_gc_2(self):
self.assertIsNone(ref())
+ def test_hamt_gc_3(self):
+ # gh-154535: the iterators must be tracked by the GC, otherwise a
+ # cycle running through one is never collected and the HAMT it
+ # holds -- and everything in it -- leaks.
+ A = HashKey(100, 'A')
+
+ container = []
+ h = hamt()
+ h = h.set(A, container)
+
+ hi = h.items()
+ self.assertTrue(gc.is_tracked(hi))
+
+ # Close the cycle: hi -> h -> container -> hi.
+ container.append(hi)
+ ref = weakref.ref(h)
+
+ del h, hi, container
+
+ gc.collect()
+ gc.collect()
+ gc.collect()
+
+ self.assertIsNone(ref())
+
def test_hamt_in_1(self):
A = HashKey(100, 'A')
AA = HashKey(100, 'A')
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst
new file mode 100644
index 00000000000000..96b9c608780c2e
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-10-11-20-00.gh-issue-155515.Lp3qWc.rst
@@ -0,0 +1,4 @@
+Track the internal HAMT iterators, which back iteration over a
+:class:`contextvars.Context`, with the garbage collector. A reference cycle
+running through such an iterator was never collected, leaking the whole
+context it iterated over.
diff --git a/Python/hamt.c b/Python/hamt.c
index 95998ae5062ac7..92ce686863964a 100644
--- a/Python/hamt.c
+++ b/Python/hamt.c
@@ -2451,6 +2451,10 @@ hamt_baseiter_tp_clear(PyObject *op)
{
PyHamtIterator *it = (PyHamtIterator*)op;
Py_CLEAR(it->hi_obj);
+ /* i_nodes holds borrowed pointers into the tree that hi_obj was keeping
+ alive, so the cursor must not be used again. A negative i_level makes
+ hamt_iterator_next() report I_END without touching i_nodes. */
+ it->hi_iter.i_level = -1;
return 0;
}
@@ -2497,6 +2501,10 @@ static Py_ssize_t
hamt_baseiter_tp_len(PyObject *op)
{
PyHamtIterator *it = (PyHamtIterator*)op;
+ if (it->hi_obj == NULL) {
+ /* tp_clear() ran on this iterator. */
+ return 0;
+ }
return it->hi_obj->h_count;
}
@@ -2517,6 +2525,7 @@ hamt_baseiter_new(PyTypeObject *type, binaryfunc yield,
PyHamtObject *o)
hamt_iterator_init(&it->hi_iter, o->h_root);
+ PyObject_GC_Track(it);
return (PyObject*)it;
}
_______________________________________________
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]