Count them by hand for a four-item list before reading the formula. The arithmetic is the entire difficulty.
items = [1, 2, 3, 4] # windows of 2: # [1, 2] starting at 0 # [2, 3] starting at 1 # [3, 4] starting at 2 # starting at 3 would give [4] - too short, so it does not count print(len(items) - 2 + 1) # -> 3 the number of start positions print(list(range(len(items) - 2 + 1))) # -> [0, 1, 2] print(list(range(2 - 3 + 1))) # -> [] a short list gives no windows
- The last start is len - size, so the count is one more than that
A window of two on four items can start at 0, 1 or 2 — three positions, which is 4 - 2 + 1. Forgetting the + 1 loses the final window; using len(items) as the bound produces short ones at the end. Counting the positions by hand once is the fastest way to be sure.
- The short case handles itself
When size is larger than the sequence, len - size + 1 is zero or negative, and range of a negative number is simply empty. No check needed — the arithmetic already says 'nothing fits'.
- This is chunk with a different step
Chunking moves the start by size each time and the pieces do not overlap; windowing moves it by one and they do. Same slicing, one different number, completely different use — worth being able to say which you need before you write either.
Each window is a slice starting one position after the last. Work out on paper which start positions are legal for a four-item list and windows of two — the count is the whole problem.
The last legal start is len(items) - size, so the starts are range(len(items) - size + 1). For each start, items[start:start + size] is the window.
Guard size < 1 first. Then a single comprehension over those start positions, slicing each one. The too-short case needs no code: range of a negative number is empty.
def windows(items, size): if size < 1: raise ValueError(...) return [items[start:start + ____] for start in range(len(items) - ____ + ____)]
def windows(items, size):
if size < 1:
raise ValueError(f"size must be at least 1, got {size}")
return [items[start:start + size]
for start in range(len(items) - size + 1)]
len - size gives the last legal start; the number of starts is one more than that because counting positions from zero always is. Two of the cases exist purely to catch its absence, and one of them — size equal to the length — is the one people never think to try.
A sequence shorter than the window gives a negative range, which is empty; an empty sequence does the same. Reaching for an if to handle those would add a branch that the arithmetic already covers — and a branch is somewhere for a bug to live.
Each window is a new list, but the items inside are the same objects. For numbers and strings that is invisible; for mutable items, changing one through a window changes it in the original and in every overlapping window at once. Worth knowing before you window a list of dictionaries.
For a large sequence this builds every window at once, which is len × size items in memory. itertools.pairwise covers the size-two case lazily, and a deque with maxlen gives you a sliding window over a stream without holding the whole thing — which is what you want for a log file or a live feed. Python 3.12 added itertools.batched for the non-overlapping case, but there is still no built-in for general windowing, which is why this function keeps getting written.
- Add a step so windows can advance by more than one — and notice that step equal to size turns this into the chunk exercise.
- Make it lazy with yield so a long stream never has to be held in memory, then work out what it can no longer promise the caller.
- Use it to write a moving average, and decide what the result should be for the first few readings where no full window exists yet.