https://github.com/python/cpython/commit/70da972f97ec799dc7d7ab069fe195455f2f81b2
commit: 70da972f97ec799dc7d7ab069fe195455f2f81b2
branch: main
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2026-02-20T14:31:58-05:00
summary:
gh-144809: Make deque copy atomic in free-threaded build (gh-144966)
files:
A Lib/test/test_free_threading/test_collections.py
A Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst
M Modules/_collectionsmodule.c
diff --git a/Lib/test/test_free_threading/test_collections.py
b/Lib/test/test_free_threading/test_collections.py
new file mode 100644
index 00000000000000..3a413ccf396d4b
--- /dev/null
+++ b/Lib/test/test_free_threading/test_collections.py
@@ -0,0 +1,29 @@
+import unittest
+from collections import deque
+from copy import copy
+from test.support import threading_helper
+
+threading_helper.requires_working_threading(module=True)
+
+
+class TestDeque(unittest.TestCase):
+ def test_copy_race(self):
+ # gh-144809: Test that deque copy is thread safe. It previously
+ # could raise a "deque mutated during iteration" error.
+ d = deque(range(100))
+
+ def mutate():
+ for i in range(1000):
+ d.append(i)
+ if len(d) > 200:
+ d.popleft()
+
+ def copy_loop():
+ for _ in range(1000):
+ copy(d)
+
+ threading_helper.run_concurrently([mutate, copy_loop])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git
a/Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst
b/Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst
new file mode 100644
index 00000000000000..62c20b7fa06d94
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst
@@ -0,0 +1 @@
+Make :class:`collections.deque` copy atomic in the :term:`free-threaded build`.
diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c
index 45ca63e6d7c77f..72865f87fc484f 100644
--- a/Modules/_collectionsmodule.c
+++ b/Modules/_collectionsmodule.c
@@ -605,29 +605,42 @@ deque_copy_impl(dequeobject *deque)
collections_state *state = find_module_state_by_def(Py_TYPE(deque));
if (Py_IS_TYPE(deque, state->deque_type)) {
dequeobject *new_deque;
- PyObject *rv;
+ Py_ssize_t n = Py_SIZE(deque);
new_deque = (dequeobject *)deque_new(state->deque_type, NULL, NULL);
if (new_deque == NULL)
return NULL;
new_deque->maxlen = old_deque->maxlen;
- /* Fast path for the deque_repeat() common case where len(deque) == 1
- *
- * It's safe to not acquire the per-object lock for new_deque; it's
- * invisible to other threads.
+
+ /* Copy elements directly by walking the block structure.
+ * This is safe because the caller holds the deque lock and
+ * the new deque is not yet visible to other threads.
*/
- if (Py_SIZE(deque) == 1) {
- PyObject *item = old_deque->leftblock->data[old_deque->leftindex];
- rv = deque_append_impl(new_deque, item);
- } else {
- rv = deque_extend_impl(new_deque, (PyObject *)deque);
- }
- if (rv != NULL) {
- Py_DECREF(rv);
- return (PyObject *)new_deque;
+ if (n > 0) {
+ block *b = old_deque->leftblock;
+ Py_ssize_t index = old_deque->leftindex;
+
+ /* Space saving heuristic. Start filling from the left */
+ assert(new_deque->leftblock == new_deque->rightblock);
+ assert(new_deque->leftindex == new_deque->rightindex + 1);
+ new_deque->leftindex = 1;
+ new_deque->rightindex = 0;
+
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *item = b->data[index];
+ if (deque_append_lock_held(new_deque, Py_NewRef(item),
+ new_deque->maxlen) < 0) {
+ Py_DECREF(new_deque);
+ return NULL;
+ }
+ index++;
+ if (index == BLOCKLEN) {
+ b = b->rightlink;
+ index = 0;
+ }
+ }
}
- Py_DECREF(new_deque);
- return NULL;
+ return (PyObject *)new_deque;
}
if (old_deque->maxlen < 0)
result = PyObject_CallOneArg((PyObject *)(Py_TYPE(deque)),
_______________________________________________
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]