From 56b615eacedbba719e688877e9ea3df0e0a86137 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 2 Feb 2026 18:35:30 +0100 Subject: [PATCH 1/7] Make itertools.combinations_with_replacement thread-safe --- .../test_free_threading/test_itertools.py | 28 ++++++++++++++++++- Modules/itertoolsmodule.c | 12 +++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index 9d366041917bb3..8236bc19283695 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -1,6 +1,6 @@ import unittest from threading import Thread, Barrier -from itertools import batched, chain, cycle +from itertools import batched, chain, combinations_with_replacement, cycle from test.support import threading_helper @@ -89,6 +89,32 @@ def work(it): barrier.reset() + @threading_helper.reap_threads + def test_combinations_with_replacement(self): + number_of_threads = 6 + number_of_iterations = 100 + data = tuple(range(3)) + + barrier = Barrier(number_of_threads) + def work(it): + barrier.wait() + while True: + try: + v = next(it) + except StopIteration: + break + + for _ in range(number_of_iterations): + cwr_iterator = combinations_with_replacement(data, 2) + worker_threads = [] + for _ in range(number_of_threads): + worker_threads.append( + Thread(target=work, args=[cwr_iterator])) + + with threading_helper.start_threads(worker_threads): + pass + + barrier.reset() if __name__ == "__main__": diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 8685eff8be65c3..1e3ea9fb5051bf 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -2587,7 +2587,7 @@ cwr_traverse(PyObject *op, visitproc visit, void *arg) } static PyObject * -cwr_next(PyObject *op) +cwr_next_lock_held(PyObject *op) { cwrobject *co = cwrobject_CAST(op); PyObject *elem; @@ -2666,6 +2666,16 @@ cwr_next(PyObject *op) return NULL; } +static PyObject * +cwr_next(PyObject *op) +{ + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(op); + result = cwr_next_lock_held(op); + Py_END_CRITICAL_SECTION() + return result; +} + static PyMethodDef cwr_methods[] = { {"__sizeof__", cwr_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ From 81390467bc16f9b4707e12ac2a4c0ef39dea55ac Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 2 Feb 2026 18:36:44 +0100 Subject: [PATCH 2/7] Make itertools.combinations_with_replacement thread-safe --- Lib/test/test_free_threading/test_itertools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index 8236bc19283695..db901f1747cb46 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -92,8 +92,8 @@ def work(it): @threading_helper.reap_threads def test_combinations_with_replacement(self): number_of_threads = 6 - number_of_iterations = 100 - data = tuple(range(3)) + number_of_iterations = 50 + data = tuple(range(2)) barrier = Barrier(number_of_threads) def work(it): From 6370bc44b957c587d83a0fd31f1881683f487455 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 2 Feb 2026 18:51:27 +0100 Subject: [PATCH 3/7] Make permutations thread-safe --- .../test_free_threading/test_itertools.py | 31 +++++++++++++++++-- Modules/itertoolsmodule.c | 12 ++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index db901f1747cb46..1b716b88453d3c 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -1,6 +1,6 @@ import unittest from threading import Thread, Barrier -from itertools import batched, chain, combinations_with_replacement, cycle +from itertools import batched, chain, combinations_with_replacement, cycle, permutations from test.support import threading_helper @@ -92,7 +92,7 @@ def work(it): @threading_helper.reap_threads def test_combinations_with_replacement(self): number_of_threads = 6 - number_of_iterations = 50 + number_of_iterations = 36 data = tuple(range(2)) barrier = Barrier(number_of_threads) @@ -116,6 +116,33 @@ def work(it): barrier.reset() + @threading_helper.reap_threads + def test_permutations(self): + number_of_threads = 6 + number_of_iterations = 36 + data = tuple(range(4)) + + barrier = Barrier(number_of_threads) + def work(it): + barrier.wait() + while True: + try: + next(it) + except StopIteration: + break + + for _ in range(number_of_iterations): + perm_iterator = permutations(data, 2) + worker_threads = [] + for _ in range(number_of_threads): + worker_threads.append( + Thread(target=work, args=[perm_iterator])) + + with threading_helper.start_threads(worker_threads): + pass + + barrier.reset() + if __name__ == "__main__": unittest.main() diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 1e3ea9fb5051bf..7e73f76bc20b58 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -2856,7 +2856,7 @@ permutations_traverse(PyObject *op, visitproc visit, void *arg) } static PyObject * -permutations_next(PyObject *op) +permutations_next_lock_held(PyObject *op) { permutationsobject *po = permutationsobject_CAST(op); PyObject *elem; @@ -2946,6 +2946,16 @@ permutations_next(PyObject *op) return NULL; } +static PyObject * +permutations_next(PyObject *op) +{ + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(op); + result = permutations_next_lock_held(op); + Py_END_CRITICAL_SECTION() + return result; +} + static PyMethodDef permuations_methods[] = { {"__sizeof__", permutations_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ From af909ce44756a6e3f08ffe812e6317f58adc6d0f Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 2 Feb 2026 18:57:43 +0100 Subject: [PATCH 4/7] remove redundant variable --- Lib/test/test_free_threading/test_itertools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index 1b716b88453d3c..f3c5c6bfa3ecfa 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -100,7 +100,7 @@ def work(it): barrier.wait() while True: try: - v = next(it) + next(it) except StopIteration: break From dbb33bdf3a6889d6ecac5936d85ca8327df889a7 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 3 Feb 2026 09:40:02 +0100 Subject: [PATCH 5/7] refactor code based on review --- .../test_free_threading/test_itertools.py | 146 ++++-------------- 1 file changed, 29 insertions(+), 117 deletions(-) diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index f3c5c6bfa3ecfa..bb6047e8669475 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -1,147 +1,59 @@ import unittest -from threading import Thread, Barrier from itertools import batched, chain, combinations_with_replacement, cycle, permutations from test.support import threading_helper threading_helper.requires_working_threading(module=True) -class ItertoolsThreading(unittest.TestCase): - - @threading_helper.reap_threads - def test_batched(self): - number_of_threads = 10 - number_of_iterations = 20 - barrier = Barrier(number_of_threads) - def work(it): - barrier.wait() - while True: - try: - next(it) - except StopIteration: - break - data = tuple(range(1000)) - for it in range(number_of_iterations): - batch_iterator = batched(data, 2) - worker_threads = [] - for ii in range(number_of_threads): - worker_threads.append( - Thread(target=work, args=[batch_iterator])) +def work_iterator(it): + while True: + try: + next(it) + except StopIteration: + break - with threading_helper.start_threads(worker_threads): - pass - barrier.reset() +class ItertoolsThreading(unittest.TestCase): @threading_helper.reap_threads - def test_cycle(self): - number_of_threads = 6 + def test_batched(self): number_of_iterations = 10 - number_of_cycles = 400 + for _ in range(number_of_iterations): + it = batched(tuple(range(1000)), 2) + threading_helper.run_concurrently(work_iterator, nthreads=10, args=[it]) - barrier = Barrier(number_of_threads) + @threading_helper.reap_threads + def test_cycle(self): def work(it): - barrier.wait() - for _ in range(number_of_cycles): - try: - next(it) - except StopIteration: - pass - - data = (1, 2, 3, 4) - for it in range(number_of_iterations): - cycle_iterator = cycle(data) - worker_threads = [] - for ii in range(number_of_threads): - worker_threads.append( - Thread(target=work, args=[cycle_iterator])) + for _ in range(400): + next(it) - with threading_helper.start_threads(worker_threads): - pass - - barrier.reset() + number_of_iterations = 6 + for _ in range(number_of_iterations): + it = cycle((1, 2, 3, 4)) + threading_helper.run_concurrently(work, nthreads=6, args=[it]) @threading_helper.reap_threads def test_chain(self): - number_of_threads = 6 - number_of_iterations = 20 - - barrier = Barrier(number_of_threads) - def work(it): - barrier.wait() - while True: - try: - next(it) - except StopIteration: - break - - data = [(1, )] * 200 - for it in range(number_of_iterations): - chain_iterator = chain(*data) - worker_threads = [] - for ii in range(number_of_threads): - worker_threads.append( - Thread(target=work, args=[chain_iterator])) - - with threading_helper.start_threads(worker_threads): - pass - - barrier.reset() + number_of_iterations = 10 + for _ in range(number_of_iterations): + it = chain(*[(1,)] * 200) + threading_helper.run_concurrently(work_iterator, nthreads=6, args=[it]) @threading_helper.reap_threads def test_combinations_with_replacement(self): - number_of_threads = 6 - number_of_iterations = 36 - data = tuple(range(2)) - - barrier = Barrier(number_of_threads) - def work(it): - barrier.wait() - while True: - try: - next(it) - except StopIteration: - break - + number_of_iterations = 6 for _ in range(number_of_iterations): - cwr_iterator = combinations_with_replacement(data, 2) - worker_threads = [] - for _ in range(number_of_threads): - worker_threads.append( - Thread(target=work, args=[cwr_iterator])) - - with threading_helper.start_threads(worker_threads): - pass - - barrier.reset() + it = combinations_with_replacement(tuple(range(2)), 2) + threading_helper.run_concurrently(work_iterator, nthreads=6, args=[it]) @threading_helper.reap_threads def test_permutations(self): - number_of_threads = 6 - number_of_iterations = 36 - data = tuple(range(4)) - - barrier = Barrier(number_of_threads) - def work(it): - barrier.wait() - while True: - try: - next(it) - except StopIteration: - break - + number_of_iterations = 6 for _ in range(number_of_iterations): - perm_iterator = permutations(data, 2) - worker_threads = [] - for _ in range(number_of_threads): - worker_threads.append( - Thread(target=work, args=[perm_iterator])) - - with threading_helper.start_threads(worker_threads): - pass - - barrier.reset() + it = permutations(tuple(range(4)), 2) + threading_helper.run_concurrently(work_iterator, nthreads=6, args=[it]) if __name__ == "__main__": From 498f314bd25db03b6cba0bfb0d332ff1c4b4ec6c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:51:07 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst new file mode 100644 index 00000000000000..df60b79a6fa771 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst @@ -0,0 +1 @@ +Make concurrent iteration over :class:`itertools.combinations_with_replacement`` and :class:`itertools.permutations` safe under free-threading. From c790b21dc39d40c911d1f8397ae459b7e43e3e9e Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 3 Feb 2026 09:57:23 +0100 Subject: [PATCH 7/7] Apply suggestion from @eendebakpt --- .../next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst index df60b79a6fa771..85e9a03426e1fc 100644 --- a/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst +++ b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst @@ -1 +1 @@ -Make concurrent iteration over :class:`itertools.combinations_with_replacement`` and :class:`itertools.permutations` safe under free-threading. +Make concurrent iteration over :class:`itertools.combinations_with_replacement` and :class:`itertools.permutations` safe under free-threading.