Skip to content

itertools: functional iteration

Tools for combining multiple iterables or selecting elements based on conditions:

from itertools import chain, compress, takewhile, dropwhile
# chain — flatten multiple iterables into one
flat = list(chain([1, 2], [3, 4], [5, 6])) # [1, 2, 3, 4, 5, 6]
flat = list(chain.from_iterable([[1, 2], [3, 4]])) # same, from a list of lists
# compress — filter by boolean mask
data = ["a", "b", "c", "d", "e"]
mask = [1, 0, 1, 0, 1]
print(list(compress(data, mask))) # ['a', 'c', 'e']
# takewhile — keep elements while condition is True, stop at first False
nums = [1, 2, 3, 4, 5, 1, 2]
print(list(takewhile(lambda x: x < 4, nums))) # [1, 2, 3]
# dropwhile — skip elements while condition is True, keep the rest
print(list(dropwhile(lambda x: x < 4, nums))) # [4, 5, 1, 2]

Note that takewhile and dropwhile operate on the first run of matching elements only — once the condition flips, they do not look back. This is why dropwhile keeps the trailing 1, 2 even though they are less than 4.


Tools for applying functions across iterables:

from itertools import starmap, accumulate
import operator
# starmap — like map(), but unpacks each element as arguments
pairs = [(2, 3), (4, 5), (10, 2)]
powers = list(starmap(pow, pairs)) # [8, 1024, 100]
sums = list(starmap(operator.add, pairs)) # [5, 9, 12]
# accumulate — running totals (or any binary operation)
sales = [100, 250, 175, 300, 225]
running_sum = list(accumulate(sales)) # [100, 350, 525, 825, 1050]
running_product = list(accumulate(sales, operator.mul)) # [100, 25000, 4375000, ...]

starmap is particularly useful when your data is already structured as argument tuples — it avoids the awkward map(lambda pair: f(*pair), pairs) pattern.


Tools for partitioning and aligning iterables:

from itertools import groupby, zip_longest
# groupby — group consecutive elements by a key (must be sorted first!)
data = [
{"name": "Alice", "dept": "Eng"},
{"name": "Bob", "dept": "Eng"},
{"name": "Carol", "dept": "HR"},
{"name": "Dave", "dept": "HR"},
]
data.sort(key=lambda x: x["dept"]) # sort before groupby — critical!
for dept, members in groupby(data, key=lambda x: x["dept"]):
names = [m["name"] for m in members]
print(f"{dept}: {names}")
# Eng: ['Alice', 'Bob']
# HR: ['Carol', 'Dave']
# zip_longest — zip iterables of different lengths, fill missing values
a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]
print(list(zip_longest(a, b, fillvalue=None)))
# [(1, 'a'), (2, 'b'), (3, 'c'), (4, None), (5, None)]

groupby has a critical requirement — the input must be sorted by the grouping key first. Unlike SQL GROUP BY, it only groups consecutive elements, so unsorted input produces incorrect results.


Tools for generating all possible arrangements of elements:

from itertools import combinations, permutations, product
items = ["A", "B", "C"]
# combinations — ordered selections, no repetition
print(list(combinations(items, 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'C')]
# permutations — all orderings, no repetition
print(list(permutations(items, 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
# product — cartesian product (like nested for loops)
print(list(product([0, 1], repeat=3)))
# [(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)]
# all 3-bit binary strings

The difference between combinations and permutations is order('A', 'B') and ('B', 'A') are the same combination but different permutations. product is equivalent to nested for loops and grows exponentially, so use it carefully with large inputs.


FunctionDescriptionLazy
chainFlatten multiple iterables
compressFilter by boolean mask
takewhileKeep while condition holds
dropwhileSkip while condition holds
starmapMap with argument unpacking
accumulateRunning totals or products
groupbyGroup consecutive elements
zip_longestZip with fill for short iterables
combinationsOrdered selections, no repeat
permutationsAll orderings, no repeat
productCartesian product