pyguides

Python itertools recipes: practical patterns for iteration

The itertools module is one of Python’s most powerful standard library tools. These Python itertools recipes show practical patterns for efficient iteration that you can use immediately. It provides fast, memory-efficient functions for working with iterators. This guide shows real-world patterns you can use immediately.

Why itertools Matters

Every function in itertools returns a lazy iterator. That means values are generated on demand, not stored in memory. For large data sets or infinite sequences, this is essential. You can process streams of data without ever loading them entirely into RAM.

Infinite Sequences

Need a counter that goes forever? Use count(). It is perfect for generating IDs, timestamps, or any sequential numbering:

import itertools

for i in itertools.count(start=100):
    print(i)
    if i >= 105:
        break
# Output: 100, 101, 102, 103, 104, 105

The default step of 1 produces consecutive integers, but count() also accepts a step argument that can be any numeric type. Floating-point steps are useful for generating coordinates, time intervals, or evenly spaced sampling points without pre-computing the entire sequence:

The step parameter controls the increment. You can use floats too:

import itertools

# Generate points for a line
for x in itertools.count(start=0.0, step=0.5):
    if x > 3:
        break
    print(f"x = {x}")
# Output: x = 0.0, x = 0.5, x = 1.0, x = 1.5, x = 2.0, x = 2.5, x = 3.0

While count() generates new values forever, sometimes you need to repeat a fixed set of values in a loop instead. The cycle() function takes an existing iterable and restarts it from the beginning each time it is exhausted, making it ideal for round-robin patterns:

The cycle() function repeats an iterable forever. Useful for cycling through colors, states, or any repeating pattern:

import itertools

states = ['idle', 'processing', 'complete']
state_cycle = itertools.cycle(states)

for _ in range(5):
    print(next(state_cycle))
# Output: idle, processing, complete, idle, processing

Cycle alternates through a finite collection endlessly, but if you only need the same single value over and over, cycle is overkill. The repeat() function produces a single value indefinitely and can optionally stop after a fixed number of repetitions:

Need the same value repeatedly? repeat() handles that efficiently:

import itertools

# Create a constant iterator
ones = itertools.repeat(1)
print(next(ones))  # 1
print(next(ones))  # 1

# Or limit the repetitions
five_twos = list(itertools.repeat(2, times=5))
print(five_twos)  # [2, 2, 2, 2, 2]

All three of these functions produce infinite iterators by default, which means a naive for-loop would never terminate. To extract a finite slice from an infinite sequence, use islice() with start, stop, and step parameters that work just like regular list slicing:

Limiting infinite iterators

Infinite iterators need limits. The islice() function works like list slicing but for iterators:

import itertools

# First 10 even numbers
evens = list(itertools.islice(itertools.count(step=2), 10))
print(evens)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Skip first 5, take next 5
numbers = range(100)
subset = list(itertools.islice(numbers, 5, 10))
print(subset)  # [5, 6, 7, 8, 9]

# Every third element, first 10
every_third = list(itertools.islice(range(100), 0, 30, 3))
print(every_third)  # [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]

islice() works with positional indexes, but sometimes the stopping condition depends on the values themselves rather than their position. The takewhile() function yields elements as long as a predicate returns true and stops at the first element that fails:

A common pattern is creating a “take while” function:

import itertools

def take_while(iterable, predicate):
    """Take elements while predicate returns True."""
    return itertools.takewhile(predicate, iterable)

numbers = range(20)
small = list(take_while(numbers, lambda x: x < 10))
print(small)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

takewhile() keeps elements at the start of an iterable. Its complement, dropwhile(), does the opposite: it discards leading elements that satisfy a condition and yields everything from the first failure onward. This is useful for skipping header rows or preamble data:

Similarly, dropwhile() skips elements until the predicate becomes false:

import itertools

data = [1, 1, 1, 2, 3, 1, 4]
remaining = list(itertools.dropwhile(lambda x: x == 1, data))
print(remaining)  # [2, 3, 1, 4]

dropwhile() stops skipping after the first non-matching value, so later matching values are kept. Notice how the final 1 in the output survived. Having covered the tools for slicing and filtering a single iterable, the next set of functions combine multiple iterables into one:

Combining Iterables

The chain() function connects multiple iterables into one:

import itertools

list1 = [1, 2, 3]
list2 = [4, 5, 6]
tuple1 = ('a', 'b')

combined = list(itertools.chain(list1, list2, tuple1))
print(combined)  # [1, 2, 3, 4, 5, 6, 'a', 'b']

chain() takes individual iterables as positional arguments and concatenates them in order. When you already have a single iterable of iterables, like a list of lists, unpacking with * works but chain.from_iterable() handles that case directly:

Use chain.from_iterable() to flatten nested iterables:

import itertools

nested = [[1, 2], [3, 4], [5]]
flat = list(itertools.chain.from_iterable(nested))
print(flat)  # [1, 2, 3, 4, 5]

Flattening with chain.from_iterable() preserves every element from every sub-iterable. When you want to pair corresponding elements from multiple iterables instead of concatenating them, the built-in zip() stops at the shortest input. zip_longest() fills in the gaps:

For uneven-length iterables, zip_longest() pads with a fill value:

import itertools

a = [1, 2, 3]
b = ['a', 'b']

paired = list(itertools.zip_longest(a, b, fillvalue='?'))
print(paired)  # [(1, 'a'), (2, 'b'), (3, '?')]

The fillvalue parameter controls what placeholder is inserted for missing elements, defaulting to None. Shifting from combination tools to computation, the accumulate() function applies a binary operation cumulatively across an iterable, producing a running result:

Accumulation Patterns

The accumulate() function computes running totals by default, but accepts any binary function:

import itertools

data = [1, 2, 3, 4, 5]

# Running sum
print(list(itertools.accumulate(data)))
# Output: [1, 3, 6, 10, 15]

# Running maximum
print(list(itertools.accumulate(data, func=max)))
# Output: [1, 2, 3, 4, 5]

accumulate() defaults to addition, giving you a running total, but passing any two-argument function through the func parameter changes the accumulation strategy. The max example shows how each step yields the largest value seen so far. For more advanced cases, you can funnel the accumulated values into your own stateful function to compute running statistics:

Track running statistics:

import itertools

def running_stats():
    """Generator that yields running mean and std dev."""
    count = 0
    mean = 0
    m2 = 0
    
    def update(value):
        nonlocal count, mean, m2
        count += 1
        delta = value - mean
        mean += delta / count
        delta2 = value - mean
        m2 += delta * delta2
        variance = m2 / count if count > 1 else 0
        return mean, variance ** 0.5
    
    return update

stats = running_stats()
for val in [10, 20, 30, 40]:
    mean, std = stats(val)
    print(f"value={val}, mean={mean:.2f}, std={std:.2f}")

This running statistics tracker uses a closure to maintain state between calls, computing the running mean and standard deviation with Welford’s online algorithm in a single pass. The accumulate function feeds each value into the closure, which updates the internal counters and returns the current statistics.

Grouping with groupby

The groupby() function groups consecutive elements. Remember: the input must be sorted by the key:

import itertools

data = ['a', 'a', 'b', 'b', 'b', 'c', 'a', 'a']

for key, group in itertools.groupby(data):
    print(f"'{key}': {list(group)}")
# Output:
# 'a': ['a', 'a']
# 'b': ['b', 'b', 'b']
# 'c': ['c']
# 'a': ['a', 'a']

Notice that the two 'a' runs appear as separate groups because groupby only merges consecutive matching elements. To consolidate all matching values into a single group regardless of position, sort the input first by the grouping key:

# Group by length (must sort first)
words = sorted(['hi', 'hello', 'hey', 'there', 'yo'], key=len)
for length, group in itertools.groupby(words, key=len):
    print(f"length {length}: {list(group)}")

Groupby with a sorted input collapses all values sharing the same key into one group. A practical application of groupby without sorting is detecting runs of identical consecutive values, which tells you how long each stretch of repeated data lasts:

A practical use: detecting changes in a sequence:

import itertools

def runs(sequence):
    """Yield (value, count) for each consecutive run."""
    for key, group in itertools.groupby(sequence):
        yield key, sum(1 for _ in group)

data = [1, 1, 1, 2, 2, 1, 1, 1, 1]
print(list(runs(data)))
# Output: [(1, 3), (2, 2), (1, 4)]

The runs() function counts consecutive identical values, revealing the structure of the sequence. Moving from grouping to generation, itertools provides product, permutations, and combinations for producing all possible arrangements of a set of values. These are especially useful for generating test cases:

Combinatorics for Testing

Generate all combinations for test cases:

import itertools

# All combinations of two dice
dice = [1, 2, 3, 4, 5, 6]
rolls = list(itertools.product(dice, repeat=2))
print(f"Total rolls: {len(rolls)}")  # 36

# All possible boolean flags (4 flags = 16 combinations)
flags = [False, True]
states = list(itertools.product(flags, repeat=4))
print(f"Total states: {len(states)}")  # 16

product() computes the Cartesian product, generating every possible combination of values across the input iterables. When the order of items matters rather than just their presence, permutations() enumerates all possible orderings of a single collection:

Generate permutations for ordering problems:

import itertools

# All orderings of 4 items
items = ['a', 'b', 'c', 'd']
perms = list(itertools.permutations(items))
print(f"Total permutations: {len(perms)}")  # 24

Permutations consider order significant, so (a,b,c) and (c,b,a) count as distinct arrangements. When order does not matter and you only care about which items appear together in a set, combinations() generates the subsets without regard for ordering:

Use combinations for selection problems:

import itertools

# All 3-card hands from a deck
ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
hands = list(itertools.combinations(ranks, 3))
print(f"Total 3-card hands: {len(hands)}")  # 2860

Combinations avoid generating permutations that differ only in order, so they are far more efficient when selection is all that matters. With the core itertools primitives covered, the following practical recipes show how to compose them into higher-level abstractions for common programming tasks:

Real-World Recipes

Pagination

import itertools

def paginate(items, page_size):
    """Yield pages of items."""
    it = iter(items)
    while True:
        page = list(itertools.islice(it, page_size))
        if not page:
            break
        yield page

data = range(25)
for i, page in enumerate(paginate(data, 7), 1):
    print(f"Page {i}: {page}")
# Output:
# Page 1: [0, 1, 2, 3, 4, 5, 6]
# Page 2: [7, 8, 9, 10, 11, 12, 13]
# Page 3: [14, 15, 16, 17, 18, 19, 20]
# Page 4: [21, 22, 23, 24]

The paginate function uses islice in a loop to carve the input into fixed-size chunks, yielding each page as a list. This pattern handles any iterable without loading the entire dataset into memory. The sliding window pattern creates overlapping views of consecutive elements, which is useful for computing moving averages or detecting trends in time-series data:

Sliding Window

import itertools

def sliding_window(sequence, size):
    """Create sliding windows of given size."""
    it = iter(sequence)
    window = list(itertools.islice(it, size))
    if len(window) == size:
        yield tuple(window)
    for item in it:
        window = window[1:] + [item]
        yield tuple(window)

numbers = [1, 2, 3, 4, 5]
for window in sliding_window(numbers, 3):
    print(window)
# Output: (1, 2, 3), (2, 3, 4), (3, 4, 5)

The sliding window yields tuples of consecutive elements by sliding a fixed-width view across the sequence one element at a time. The round-robin scheduler pairs teams for tournament play by rotating the lineup while keeping the first team fixed:

Round-Robin Scheduling

import itertools

def round_robin(*teams):
    """Schedule games in round-robin format."""
    n = len(teams)
    for i in range(n - 1):
        round_games = []
        for j in range(n // 2):
            home = teams[j]
            away = teams[n - 1 - j]
            round_games.append((home, away))
        yield round_games
        teams = teams[1:] + teams[:1]

teams = ['A', 'B', 'C', 'D']
for i, games in enumerate(round_robin(*teams), 1):
    print(f"Round {i}: {games}")

The round-robin function rotates the team list after each round, generating a complete set of pairings without repeats. For the final recipe, here is a deduplication filter that preserves insertion order while removing all but the first occurrence of each value:

Filtering Duplicates

import itertools

def unique_everseen(iterable):
    """Remove duplicates, keeping first occurrence."""
    seen = set()
    for item in iterable:
        if item not in seen:
            seen.add(item)
            yield item

data = [1, 2, 2, 3, 1, 4, 3, 5]
print(list(unique_everseen(data)))  # [1, 2, 3, 4, 5]

Performance Notes

All itertools functions return iterators, not lists. This means memory usage stays constant regardless of data size. You must consume values with next(), loops, or conversion. Chaining multiple itertools is efficient.

For very large datasets, itertools can be the difference between running out of memory and successful processing. The lazy evaluation is the key.

See Also