diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index bb6047e8669475..0a99fb0710c2c2 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -1,5 +1,5 @@ import unittest -from itertools import batched, chain, combinations_with_replacement, cycle, permutations +from itertools import batched, chain, combinations_with_replacement, cycle, pairwise, permutations from test.support import threading_helper @@ -48,6 +48,13 @@ def test_combinations_with_replacement(self): it = combinations_with_replacement(tuple(range(2)), 2) threading_helper.run_concurrently(work_iterator, nthreads=6, args=[it]) + @threading_helper.reap_threads + def test_pairwise(self): + number_of_iterations = 10 + for _ in range(number_of_iterations): + it = pairwise(tuple(range(100))) + threading_helper.run_concurrently(work_iterator, nthreads=10, args=[it]) + @threading_helper.reap_threads def test_permutations(self): number_of_iterations = 6 diff --git a/Misc/NEWS.d/next/Library/2026-02-04-21-17-43.gh-issue-123471.tVAWYF.rst b/Misc/NEWS.d/next/Library/2026-02-04-21-17-43.gh-issue-123471.tVAWYF.rst new file mode 100644 index 00000000000000..ceb84de8ee73d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-04-21-17-43.gh-issue-123471.tVAWYF.rst @@ -0,0 +1 @@ +Make concurrent iteration over :class:`itertools.pairwise` safe under free-threading. diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 7e73f76bc20b58..fe06cd5f4107a0 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -343,7 +343,7 @@ pairwise_traverse(PyObject *op, visitproc visit, void *arg) } static PyObject * -pairwise_next(PyObject *op) +pairwise_next_lock_held(PyObject *op) { pairwiseobject *po = pairwiseobject_CAST(op); PyObject *it = po->it; @@ -366,7 +366,7 @@ pairwise_next(PyObject *op) return NULL; } } - Py_INCREF(old); + Py_INCREF(old); // needed because of reentant calls via call to the iterator new = (*Py_TYPE(it)->tp_iternext)(it); if (new == NULL) { Py_CLEAR(po->it); @@ -380,7 +380,7 @@ pairwise_next(PyObject *op) Py_INCREF(result); PyObject *last_old = PyTuple_GET_ITEM(result, 0); PyObject *last_new = PyTuple_GET_ITEM(result, 1); - PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); + PyTuple_SET_ITEM(result, 0, old); // consume the reference from old PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); Py_DECREF(last_old); Py_DECREF(last_new); @@ -391,13 +391,25 @@ pairwise_next(PyObject *op) else { result = PyTuple_New(2); if (result != NULL) { - PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); + PyTuple_SET_ITEM(result, 0, old); PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); } + else { + Py_DECREF(old); + } } Py_XSETREF(po->old, new); - Py_DECREF(old); + return result; +} + +static PyObject * +pairwise_next(PyObject *op) +{ + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(op); + result = pairwise_next_lock_held(op); + Py_END_CRITICAL_SECTION() return result; }