From 82c68c8fe584f9f18bbbd154267ed9cc38dfb1cf Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Sat, 28 Feb 2026 09:24:16 +0100 Subject: [PATCH 1/4] gh-145300: Add __length_hint__ for itertools.islice --- Lib/test/test_itertools.py | 10 ++++++ ...-02-28-09-23-41.gh-issue-145300.JlCq0D.rst | 1 + Modules/itertoolsmodule.c | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-28-09-23-41.gh-issue-145300.JlCq0D.rst diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index dc64288085fa74..7a9fdda4c1e8c2 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -1213,6 +1213,16 @@ def __index__(self): self.assertEqual(list(islice(range(100), IntLike(10), IntLike(50), IntLike(5))), list(range(10,50,5))) + # Test __length_hint__ + self.assertEqual(islice(range(10), 1).__length_hint__(), 1) + self.assertEqual(islice(range(10), 1, 2).__length_hint__(), 1) + self.assertEqual(islice(range(10), 1, 2, 2).__length_hint__(), 1) + self.assertEqual(islice(range(10), 0, 4, 2).__length_hint__(), 2) + self.assertEqual(islice(range(10), 2, None, 1).__length_hint__(), 8) + self.assertEqual(islice(range(10), 2, None, 2).__length_hint__(), 4) + self.assertEqual(islice(range(10), 3, None, 2).__length_hint__(), 4) + self.assertEqual(islice(range(10), 2, 2, 1).__length_hint__(), 0) + def test_takewhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(takewhile(underten, data)), [1, 3, 5]) diff --git a/Misc/NEWS.d/next/Library/2026-02-28-09-23-41.gh-issue-145300.JlCq0D.rst b/Misc/NEWS.d/next/Library/2026-02-28-09-23-41.gh-issue-145300.JlCq0D.rst new file mode 100644 index 00000000000000..da662cec4445e4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-28-09-23-41.gh-issue-145300.JlCq0D.rst @@ -0,0 +1 @@ +Add ``__length_hint__`` to :class:`itertools.islice`. diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index ff0e2fd2b3569d..b7232618c72d8c 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1660,6 +1660,35 @@ islice_next(PyObject *op) return NULL; } +static PyObject * +islice_length_hint(PyObject *op, PyObject *Py_UNUSED(dummy)) +{ + isliceobject *lz = isliceobject_CAST(op); + + if (lz->stop >= 0 && lz->stop <= lz->next) { + return PyLong_FromSsize_t(0); + } + + Py_ssize_t remaining; + if (lz->stop == -1) { + Py_ssize_t hint = PyObject_LengthHint(lz->it, 0); + if (hint < 0) { + /* propagate exception */ + return NULL; + } + remaining = hint - lz->next; + } else { + remaining = lz->stop - lz->next; + } + + if (remaining <= 0) { + return PyLong_FromSsize_t(0); + } + + Py_ssize_t steps = (remaining + lz->step - 1) / lz->step; + return PyLong_FromSsize_t(steps); +} + PyDoc_STRVAR(islice_doc, "islice(iterable, stop) --> islice object\n\ islice(iterable, start, stop[, step]) --> islice object\n\ @@ -1671,6 +1700,11 @@ specified as another value, step determines how many values are\n\ skipped between successive calls. Works like a slice() on a list\n\ but returns an iterator."); +static PyMethodDef islice_methods[] = { + {"__length_hint__", islice_length_hint, METH_NOARGS, NULL}, + {NULL, NULL}, +}; + static PyType_Slot islice_slots[] = { {Py_tp_dealloc, islice_dealloc}, {Py_tp_getattro, PyObject_GenericGetAttr}, @@ -1680,6 +1714,7 @@ static PyType_Slot islice_slots[] = { {Py_tp_iternext, islice_next}, {Py_tp_new, islice_new}, {Py_tp_free, PyObject_GC_Del}, + {Py_tp_methods, islice_methods}, {0, NULL}, }; From e14996a71f5c2deed77f60d9b400a634a036e88d Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Sat, 28 Feb 2026 10:53:30 +0100 Subject: [PATCH 2/4] fix: handle exhausted iterators --- Lib/test/test_itertools.py | 8 ++++++++ Modules/itertoolsmodule.c | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index 7a9fdda4c1e8c2..b20e1e0baab979 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -1223,6 +1223,14 @@ def __index__(self): self.assertEqual(islice(range(10), 3, None, 2).__length_hint__(), 4) self.assertEqual(islice(range(10), 2, 2, 1).__length_hint__(), 0) + it = islice(iter(range(5)), None) + list(it) # exhaust + self.assertEqual(it.__length_hint__(), 0) + + it = islice(iter(range(5)), 3) + list(it) # exhaust + self.assertEqual(it.__length_hint__(), 0) + def test_takewhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(takewhile(underten, data)), [1, 3, 5]) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index b7232618c72d8c..84dd602cf421f6 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1665,7 +1665,7 @@ islice_length_hint(PyObject *op, PyObject *Py_UNUSED(dummy)) { isliceobject *lz = isliceobject_CAST(op); - if (lz->stop >= 0 && lz->stop <= lz->next) { + if (lz->it == NULL || (lz->stop >= 0 && lz->stop <= lz->next)) { return PyLong_FromSsize_t(0); } From 9f1b3a63bacaee581d6b053dccfd92dcbc4d70f8 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Sat, 28 Feb 2026 10:56:15 +0100 Subject: [PATCH 3/4] fix: take count into account when using underlying hint --- Lib/test/test_itertools.py | 11 +++++++++++ Modules/itertoolsmodule.c | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index b20e1e0baab979..46d20c04d713f6 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -1231,6 +1231,17 @@ def __index__(self): list(it) # exhaust self.assertEqual(it.__length_hint__(), 0) + it = islice(iter(range(10)), 2, None, 2) + self.assertEqual(it.__length_hint__(), 4) # indices 2, 4, 6, 8 + next(it) # yields 2 + self.assertEqual(it.__length_hint__(), 3) # indices 4, 6, 8 + next(it) # yields 4 + self.assertEqual(it.__length_hint__(), 2) # indices 6, 8 + next(it) # yields 6 + self.assertEqual(it.__length_hint__(), 1) # index 8 + next(it) # yields 8 + self.assertEqual(it.__length_hint__(), 0) + def test_takewhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(takewhile(underten, data)), [1, 3, 5]) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 84dd602cf421f6..807d8fd86c9ace 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1676,7 +1676,7 @@ islice_length_hint(PyObject *op, PyObject *Py_UNUSED(dummy)) /* propagate exception */ return NULL; } - remaining = hint - lz->next; + remaining = hint - (lz->next - lz->cnt); } else { remaining = lz->stop - lz->next; } From cb3866f393ff3aaa0c1506f17ff2c449ddff2552 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Sat, 28 Feb 2026 10:57:34 +0100 Subject: [PATCH 4/4] fix: avoid overflow --- Modules/itertoolsmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 807d8fd86c9ace..4066beaba61ae7 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1685,7 +1685,7 @@ islice_length_hint(PyObject *op, PyObject *Py_UNUSED(dummy)) return PyLong_FromSsize_t(0); } - Py_ssize_t steps = (remaining + lz->step - 1) / lz->step; + Py_ssize_t steps = 1 + (remaining - 1) / lz->step; return PyLong_FromSsize_t(steps); }