itertools.islice Start/Stop Are Relative, Not Fixed Indices (tested)
itertools.islice(it, start, stop) reads like list[start:stop], so it's tempting to assume calling it again with the same numbers gives you the same slice back. We measured what actually happens, and the two calls return entirely different data.
1. Calling islice twice with the same arguments gives two different results
import itertools
it = iter(range(20))
slice1 = list(itertools.islice(it, 2, 5))
print("first call: ", slice1)
slice2 = list(itertools.islice(it, 2, 5)) # same start, same stop
print("second call:", slice2)
first call: [2, 3, 4]
second call: [7, 8, 9]
Measured: the second call does not repeat [2, 3, 4]. islice()'s start and stop are counted from wherever the underlying iterator currently is, not from its original beginning. The first call consumes items 0-4 (skipping 0-1, yielding 2-4); by the time the second call runs, the iterator is already sitting at item 5, so "skip 2, take 3" lands on items 7-9.
2. A list behaves the way islice looks like it should
data = list(range(20))
print(data[2:5])
print(data[2:5]) # same slice every time
[2, 3, 4]
[2, 3, 4]
Measured: ordinary list slicing with [2:5] is a fixed, repeatable window into the sequence -- calling it twice always returns the same items. islice() looks like the same syntax but operates on a one-way iterator, so there's no fixed window to return to; each call picks up exactly where the last one left the iterator.
3. islice doesn't support negative indices either
import itertools
it = iter(range(10))
list(itertools.islice(it, -3, None))
ValueError: Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize.
Measured: unlike list slicing's data[-3:] for "last 3 items," a negative start raises a clear ValueError immediately -- at least this one fails loudly rather than silently, which makes it easier to catch than the repeated-call surprise above.
4. Fix: treat islice as consuming, and compute real pagination offsets yourself
import itertools
it = iter(range(20))
page_size = 3
for page_start in range(0, 9, page_size):
page = list(itertools.islice(it, page_size)) # always "next N", not "items[start:stop]"
print(f"page at {page_start}:", page)
page at 0: [0, 1, 2]
page at 3: [3, 4, 5]
page at 6: [6, 7, 8]
Measured: dropping the start argument and only ever passing a count (islice(it, n)) makes the "consume the next N" behavior explicit and avoids the trap of reusing fixed (start, stop) numbers that assume a rewindable sequence.
Frequently asked questions
Does itertools.islice(it, 2, 5) always return items 2 through 4?
No -- we measured this directly: start and stop are relative to the iterator's current position when islice() is called, not to its original beginning. Calling it again later with the same numbers returns later items, not the same ones.
Why did my itertools.islice call return different data the second time?
Because the underlying iterator had already advanced past the point you think of as 'the start.' islice() has no way to rewind a plain iterator, so start and stop always count forward from wherever reading last stopped.
Can I use a negative start with itertools.islice like list[-3:]?
No -- we confirmed it raises ValueError: Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize immediately, rather than silently misbehaving.
How do I paginate through an iterator correctly with islice?
Call islice(it, n) with just a count, inside a loop, and let it consume 'the next n items' each time -- don't pass fixed (start, stop) numbers expecting them to index into the original sequence.
Does this affect lists and tuples, or only iterators?
If you pass a list or tuple to islice(), it's still iterated one-way internally and has the same relative-position behavior -- the fixed, repeatable-slice behavior only applies to plain Python slicing syntax like data[2:5], not to islice().
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in itertools module: called itertools.islice() twice in a row on the same iterator with identical start/stop arguments, and separately tried a negative start index. PlainHeaders is our extension; Python's itertools module is built in.