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]

Reply via email to