From 26fd46e9a1f8d591dc87781f7eb0f35ad8fe6bb6 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 22 Mar 2026 07:18:35 -0700 Subject: [PATCH 1/5] Fix infinite generator hanging on extended slice assignment (gh-146268) --- Lib/test/list_tests.py | 49 +++++++++++ ...-03-22-07-24-41.gh-issue-146268.-f860z.rst | 3 + Objects/listobject.c | 88 +++++++++++++++++-- 3 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index e76f79c274e744..c3a9d425021ec2 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -202,6 +202,55 @@ def test_slice_assign_iterator(self): x[:] = reversed(range(3)) self.assertEqual(x, self.type2test([2, 1, 0])) + def test_extended_slice_assign_infinite_iterator(self): + # gh-146268: assigning an infinite iterator to an extended slice + # (step != 1) should raise ValueError, not hang indefinitely. + def infinite_gen(): + while True: + yield "foo" + + a = self.type2test(range(4)) + with self.assertRaises(ValueError) as cm: + a[::2] = infinite_gen() + self.assertIn("yielded more items than extended slice of size 2", + str(cm.exception)) + # list should be unchanged after the failed assignment + self.assertEqual(a, self.type2test(range(4))) + + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[::3] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Negative step with infinite iterator + a = self.type2test(range(6)) + with self.assertRaises(ValueError): + a[::-2] = infinite_gen() + self.assertEqual(a, self.type2test(range(6))) + + def test_extended_slice_assign_iterator(self): + # Assigning a finite iterator with the correct length to an + # extended slice should work. + a = self.type2test(range(10)) + a[::2] = iter(range(5)) + self.assertEqual(a, self.type2test([0, 1, 1, 3, 2, 5, 3, 7, 4, 9])) + + # Too few items from an iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError) as cm: + a[::2] = iter(range(3)) + self.assertIn("sequence of size 3 to extended slice of size 5", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(10))) + + # Too many items from an iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError) as cm: + a[::2] = iter(range(10)) + self.assertIn("yielded more items than extended slice of size 5", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(10))) + def test_delslice(self): a = self.type2test([0, 1]) del a[1:2] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst new file mode 100644 index 00000000000000..6fe9a3db153672 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-22-07-24-41.gh-issue-146268.-f860z.rst @@ -0,0 +1,3 @@ +Fixed extended slice assignment (e.g. ``l[::2] = x``) hanging when the +right-hand side is an infinite iterator. A bounded iteration is now used so +that a :exc:`ValueError` is raised promptly. Patch by Charles Machalow. diff --git a/Objects/listobject.c b/Objects/listobject.c index 654b8130e70840..b0b6e7b3cf67a0 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3821,12 +3821,70 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) PyObject **garbage, **seqitems, **selfitems; Py_ssize_t i; size_t cur; + int bounded_iter = 0; /* protect against a[::-1] = a */ if (self == (PyListObject*)value) { seq = list_slice_lock_held((PyListObject *)value, 0, Py_SIZE(value)); } + else if (step != 1 && + !PyList_CheckExact(value) && + !PyTuple_CheckExact(value)) + { + /* For extended slices (step != 1) with arbitrary iterables, + use bounded iteration to avoid hanging on infinite + iterators (gh-146268). We compute a preliminary slice + length to cap the number of items we collect. The real + slice length is recomputed afterwards because the + iterable's __next__ may mutate the list. */ + Py_ssize_t tmp_start = start, tmp_stop = stop; + Py_ssize_t slicelength_bound = adjust_slice_indexes( + self, &tmp_start, &tmp_stop, step); + + PyObject *it = PyObject_GetIter(value); + if (it == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_SetString(PyExc_TypeError, + "must assign iterable " + "to extended slice"); + } + return -1; + } + Py_ssize_t alloc = slicelength_bound + 1; + if (alloc <= 0) { + /* Overflow or zero-length slice; still collect at + least 1 item so the size check can detect a + non-empty iterable. */ + alloc = 1; + } + seq = PyList_New(alloc); + if (seq == NULL) { + Py_DECREF(it); + return -1; + } + Py_ssize_t j; + for (j = 0; j < alloc; j++) { + PyObject *v = PyIter_Next(it); + if (v == NULL) { + if (PyErr_Occurred()) { + /* Discard unfilled slots before decref */ + Py_SET_SIZE(seq, j); + Py_DECREF(seq); + Py_DECREF(it); + return -1; + } + break; + } + PyList_SET_ITEM(seq, j, v); + } + Py_DECREF(it); + /* Shrink to the number of items actually collected */ + if (j < alloc) { + Py_SET_SIZE(seq, j); + } + bounded_iter = 1; + } else { seq = PySequence_Fast(value, "must assign iterable " @@ -3845,12 +3903,30 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) } if (PySequence_Fast_GET_SIZE(seq) != slicelength) { - PyErr_Format(PyExc_ValueError, - "attempt to assign sequence of " - "size %zd to extended slice of " - "size %zd", - PySequence_Fast_GET_SIZE(seq), - slicelength); + if (bounded_iter && + PySequence_Fast_GET_SIZE(seq) > slicelength) { + PyErr_Format(PyExc_ValueError, + "attempt to assign iterable that yielded " + "more items than extended slice of " + "size %zd", + slicelength); + } + else if (bounded_iter) { + PyErr_Format(PyExc_ValueError, + "attempt to assign sequence of " + "size %zd to extended slice of " + "size %zd", + PySequence_Fast_GET_SIZE(seq), + slicelength); + } + else { + PyErr_Format(PyExc_ValueError, + "attempt to assign sequence of " + "size %zd to extended slice of " + "size %zd", + PySequence_Fast_GET_SIZE(seq), + slicelength); + } Py_DECREF(seq); return -1; } From 21b973e0452927b17dbd1018fb9ccce59f13da74 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 22 Mar 2026 07:21:37 -0700 Subject: [PATCH 2/5] Fix dup if condition --- Objects/listobject.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index b0b6e7b3cf67a0..1003f3f324e078 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3911,14 +3911,6 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) "size %zd", slicelength); } - else if (bounded_iter) { - PyErr_Format(PyExc_ValueError, - "attempt to assign sequence of " - "size %zd to extended slice of " - "size %zd", - PySequence_Fast_GET_SIZE(seq), - slicelength); - } else { PyErr_Format(PyExc_ValueError, "attempt to assign sequence of " From 1c24daea7bcab6bdfe445afd4fb8348cf50d57e0 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 22 Mar 2026 07:22:29 -0700 Subject: [PATCH 3/5] bounded_iter should be a bool --- Objects/listobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/listobject.c b/Objects/listobject.c index 1003f3f324e078..77c40f6958d6cd 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3821,7 +3821,7 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) PyObject **garbage, **seqitems, **selfitems; Py_ssize_t i; size_t cur; - int bounded_iter = 0; + bool bounded_iter = false; /* protect against a[::-1] = a */ if (self == (PyListObject*)value) { @@ -3883,7 +3883,7 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) if (j < alloc) { Py_SET_SIZE(seq, j); } - bounded_iter = 1; + bounded_iter = true; } else { seq = PySequence_Fast(value, From e88d66f1670f804897015cbda052f5116bc9b634 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 22 Mar 2026 07:25:55 -0700 Subject: [PATCH 4/5] Add test for code path that didn't have test --- Lib/test/list_tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index c3a9d425021ec2..50dc0a30536f3b 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -251,6 +251,15 @@ def test_extended_slice_assign_iterator(self): str(cm.exception)) self.assertEqual(a, self.type2test(range(10))) + def test_extended_slice_assign_non_iterable(self): + # Assigning a non-iterable to an extended slice should raise TypeError. + a = self.type2test(range(4)) + with self.assertRaises(TypeError) as cm: + a[::2] = 42 + self.assertIn("must assign iterable to extended slice", + str(cm.exception)) + self.assertEqual(a, self.type2test(range(4))) + def test_delslice(self): a = self.type2test([0, 1]) del a[1:2] From c1ad027b647ce39156021f29655b249459023d38 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 22 Mar 2026 10:06:37 -0700 Subject: [PATCH 5/5] PR feedback --- Lib/test/list_tests.py | 24 ++++++++++++++++++++++++ Objects/listobject.c | 2 ++ 2 files changed, 26 insertions(+) diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index 50dc0a30536f3b..79c5bdcfffecc0 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -228,6 +228,30 @@ def infinite_gen(): a[::-2] = infinite_gen() self.assertEqual(a, self.type2test(range(6))) + # Explicit start and stop with infinite iterator + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[1:8:2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Explicit start only + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[2::3] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Explicit stop only + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[:7:2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + + # Negative step with explicit start and stop + a = self.type2test(range(10)) + with self.assertRaises(ValueError): + a[8:1:-2] = infinite_gen() + self.assertEqual(a, self.type2test(range(10))) + def test_extended_slice_assign_iterator(self): # Assigning a finite iterator with the correct length to an # extended slice should work. diff --git a/Objects/listobject.c b/Objects/listobject.c index 77c40f6958d6cd..2c2e99bac2e696 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3845,9 +3845,11 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value) PyObject *it = PyObject_GetIter(value); if (it == NULL) { if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyObject *exc = PyErr_GetRaisedException(); PyErr_SetString(PyExc_TypeError, "must assign iterable " "to extended slice"); + _PyErr_ChainExceptions1(exc); } return -1; }