backend / python core / stdlib / 02_itertools.md

itertools — efficient iterator combinators

3 min read source

itertools — efficient iterator combinators

itertools provides building blocks for working with iterators lazily and memory-efficiently. Most operations are O(1) memory regardless of input size.

Infinite iterators

import itertools as it

it.count(start=0, step=1)        # 0, 1, 2, 3, ...
it.cycle([1, 2, 3])              # 1, 2, 3, 1, 2, 3, ...
it.repeat("hi", times=3)         # 'hi', 'hi', 'hi'
it.repeat(0)                     # 0, 0, 0, ...    (no `times` = infinite)

Use with islice to bound them. it.count() paired with zip is the modern enumerate analog.

Slicing iterators

list(it.islice(it.count(), 5))       # [0, 1, 2, 3, 4]
list(it.islice(it.count(), 5, 10))   # [5, 6, 7, 8, 9]

Like list slicing but works on any iterable, including infinite ones.

Combining iterables

list(it.chain([1, 2], [3, 4], [5]))         # [1, 2, 3, 4, 5]
list(it.chain.from_iterable([[1, 2], [3, 4]]))   # [1, 2, 3, 4]  ← flattens

list(zip([1, 2, 3], ['a', 'b', 'c']))       # [(1, 'a'), (2, 'b'), (3, 'c')]
list(it.zip_longest([1, 2, 3], ['a'], fillvalue='?'))   # [(1, 'a'), (2, '?'), (3, '?')]

chain.from_iterable is the fast way to flatten one level of nesting.

Filtering and selection

list(it.compress("ABCDEF", [1, 0, 1, 0, 1, 1]))   # ['A', 'C', 'E', 'F']
list(it.takewhile(lambda x: x < 5, [1, 4, 6, 4, 1]))   # [1, 4]
list(it.dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1]))   # [6, 4, 1]
list(it.filterfalse(lambda x: x % 2, range(10)))       # [0, 2, 4, 6, 8]

Aggregation

list(it.accumulate([1, 2, 3, 4]))                  # [1, 3, 6, 10] — running sum
list(it.accumulate([1, 2, 3, 4], operator.mul))    # [1, 2, 6, 24] — running product
list(it.accumulate([5, 3, 7, 2], max))             # [5, 5, 7, 7] — running max

accumulate is great for prefix sums (DP problems, time series).

Grouping

data = [("a", 1), ("a", 2), ("b", 3), ("b", 4), ("a", 5)]

for key, group in it.groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# a [('a', 1), ('a', 2)]
# b [('b', 3), ('b', 4)]
# a [('a', 5)]            ← gotcha: groupby groups CONSECUTIVE elements only

Gotcha: groupby only groups adjacent equal keys. To group all matches by key, sort first:

data.sort(key=lambda x: x[0])
for key, group in it.groupby(data, key=lambda x: x[0]):
    ...

Combinatorics

list(it.product([1, 2], ['a', 'b']))            # [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
list(it.product(range(2), repeat=3))             # all 3-bit binary tuples (0,0,0)...(1,1,1)

list(it.permutations([1, 2, 3]))                 # 6 orderings
list(it.permutations([1, 2, 3], 2))              # 6 ordered pairs

list(it.combinations([1, 2, 3, 4], 2))           # [(1,2), (1,3), (1,4), (2,3), (2,4), (3,4)]
list(it.combinations_with_replacement([1, 2], 2))  # [(1,1), (1,2), (2,2)]

These materialize into iterators — be careful with large inputs (permutations(range(20)) is 20! items).

Splitting iterators with tee

a, b = it.tee([1, 2, 3, 4], 2)
sum(a) + max(b)         # 10 + 4 = 14

tee buffers as much as needed for the slowest consumer. Don’t use it if one branch will lag far behind — memory grows.

pairwise (3.10+)

list(it.pairwise([1, 2, 3, 4]))   # [(1, 2), (2, 3), (3, 4)]

Useful for diffing consecutive elements (sliding window of size 2).

Common patterns

# Flatten one level
flat = list(it.chain.from_iterable(nested))

# Sliding window of size n (3.10+):
def windows(iterable, n):
    iters = it.tee(iterable, n)
    for i, it_ in enumerate(iters):
        next(it.islice(it_, i, i), None)
    return zip(*iters)

list(windows([1, 2, 3, 4, 5], 3))    # [(1,2,3), (2,3,4), (3,4,5)]

# Chunk into groups of n:
def chunked(iterable, n):
    it_ = iter(iterable)
    while chunk := list(it.islice(it_, n)):
        yield chunk

list(chunked(range(10), 3))    # [[0,1,2], [3,4,5], [6,7,8], [9]]

In Python 3.12+, itertools.batched does the chunking for you.

Interview angle

“Find all unique pairs in a list” → combinations. “Compute running sums” → accumulate. “Group consecutive duplicates” → groupby. “Why is chain.from_iterable faster than [x for sub in nested for x in sub]?” (Pure C; no Python-level loop.)