The Python itertools module provides a collection of fast, memory-efficient iterator building blocks — functions for combining, filtering, grouping, and generating sequences lazily, following the iterator algebra model.
Combining and transforming sequences
What you'll learn: chain() for flattening iterables, islice() for memory-efficient slicing, product() for Cartesian products.
How to read this tab: Try itertools.chain([1,2], [3,4], [5,6]) in the REPL. Then try itertools.product("AB", range(3)). These are the most immediately useful itertools functions.
itertools is a toolkit of building blocks for working with sequences — it gives you efficient, memory-friendly tools for chaining, grouping, filtering, combining, and repeating iterables.
chain.from_iterable() is more memory-efficient than chain(). itertools.chain([1,2],[3,4]) takes multiple arguments. itertools.chain.from_iterable([[1,2],[3,4]]) takes one iterable of iterables — useful when you're generating the sublists lazily.
Combining iterables
import itertools
# chain — flatten multiple iterables into one
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
combined = list(itertools.chain(a, b, c))
print(combined) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# chain.from_iterable — flatten one level
nested = [[1, 2], [3, 4], [5, 6]]
flat = list(itertools.chain.from_iterable(nested))
print(flat) # [1, 2, 3, 4, 5, 6]
# zip_longest — zip, filling missing values with fillvalue
names = ["Priya", "Rahul"]
scores = [95, 88, 72] # longer
for n, s in itertools.zip_longest(names, scores, fillvalue="N/A"):
print(f"{n}: {s}")
# Priya: 95 | Rahul: 88 | N/A: 72The mathematical difference: combinations(["A","B","C"], 2) gives AB, AC, BC (order doesn't matter, no repeats). permutations(["A","B","C"], 2) gives AB, AC, BA, BC, CA, CB (order matters). combinations_with_replacement allows repeats (AA, AB, AC, BB, BC, CC).
Permutations and combinations
import itertools
items = ["A", "B", "C"]
# product — cartesian product (nested for loops)
for p in itertools.product([0, 1], repeat=3):
print(p) # all 8 binary combinations of length 3
# permutations — all orderings
for p in itertools.permutations(items, 2):
print(p) # ('A','B'), ('A','C'), ('B','A'), ... — 6 total
# combinations — unique subsets (order doesn't matter)
for c in itertools.combinations(items, 2):
print(c) # ('A','B'), ('A','C'), ('B','C') — 3 total
# combinations_with_replacement — can repeat elements
for c in itertools.combinations_with_replacement("ABC", 2):
print(c) # ('A','A'), ('A','B'), ('A','C'), ('B','B'), ...Infinite iterators never stop. itertools.count(1) counts 1, 2, 3, ... forever. itertools.cycle([A,B,C]) repeats A, B, C, A, B, C, ... forever. Always use them with islice() or takewhile() to stop iteration: list(islice(count(1), 10)) gives [1..10].
Infinite iterators and slicing
import itertools
# count — count from start by step
for n in itertools.count(10, 2): # 10, 12, 14, ...
if n > 20: break
print(n)
# cycle — repeat a sequence forever
colours = itertools.cycle(["red", "green", "blue"])
print([next(colours) for _ in range(7)])
# ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']
# repeat — repeat a value (n times, or forever)
ones = list(itertools.repeat(1, 5)) # [1, 1, 1, 1, 1]
# islice — slice any iterator (not just sequences)
gen = (x**2 for x in range(1000))
first_ten = list(itertools.islice(gen, 10)) # [0,1,4,9,16,25,36,49,64,81]
# groupby — group consecutive same-key elements
data = [("A", 1), ("A", 2), ("B", 3), ("B", 4), ("A", 5)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
print(key, list(group))
# A [('A', 1), ('A', 2)] | B [('B', 3), ('B', 4)] | A [('A', 5)]✅ Beginner tab complete — check your understanding
- I can use itertools.chain() to flatten a list of lists
- I can use itertools.islice() to take the first N items from any iterator
- I can use itertools.product() to generate all combinations of multiple sequences
combinations, permutations, groupby, and accumulate
What you'll learn: combinations and permutations for mathematical set operations. groupby for grouping consecutive elements. accumulate for running totals.
How to read this tab: The groupby gotcha is critical: your data MUST be sorted by the key before grouping. Try it without sorting first to see the wrong result, then sort and see the correct one.
These replace common loop patterns. takewhile(pred, iterable) yields items while the predicate is true, then stops. dropwhile skips items until the predicate is false. filterfalse is the opposite of filter(). accumulate([1,2,3,4]) gives [1,3,6,10] (running total).
Functional itertools: takewhile, dropwhile, filterfalse, accumulate
import itertools
import operator
# takewhile — yield items while condition is True
nums = [1, 2, 3, 4, 5, 4, 3, 2, 1]
ascending = list(itertools.takewhile(lambda x: x < 5, nums))
print(ascending) # [1, 2, 3, 4]
# dropwhile — skip items while condition is True, then yield rest
rest = list(itertools.dropwhile(lambda x: x < 5, nums))
print(rest) # [5, 4, 3, 2, 1]
# filterfalse — yield items where predicate is False
evens = list(itertools.filterfalse(lambda x: x % 2, range(10)))
print(evens) # [0, 2, 4, 6, 8]
# accumulate — running accumulation (default: sum)
running_sum = list(itertools.accumulate([1, 2, 3, 4, 5]))
print(running_sum) # [1, 3, 6, 10, 15]
# Running max
running_max = list(itertools.accumulate([3,1,4,1,5,9,2], func=max))
print(running_max) # [3, 3, 4, 4, 5, 9, 9]
# Running product
factorial = list(itertools.accumulate(range(1,6), func=operator.mul))
print(factorial) # [1, 2, 6, 24, 120]The official docs include a "Recipes" section. These are not in itertools itself but are short combinations of itertools functions that solve common problems: pairwise(), sliding window, batched chunking, etc. Python 3.12 added many of these as actual itertools functions (itertools.batched, itertools.pairwise).
Recipes from itertools docs
import itertools
# pairwise (Python 3.10+) — overlapping pairs
from itertools import pairwise
pairs = list(pairwise([1, 2, 3, 4]))
print(pairs) # [(1,2), (2,3), (3,4)]
# batched (Python 3.12+) — batch into n-size chunks
from itertools import batched
pages = list(batched(range(10), 3))
print(pages) # [(0,1,2), (3,4,5), (6,7,8), (9,)]
# Classic recipe: sliding window
def sliding_window(iterable, n):
it = iter(iterable)
window = tuple(itertools.islice(it, n))
if len(window) == n:
yield window
for x in it:
window = window[1:] + (x,)
yield window
print(list(sliding_window([1,2,3,4,5], 3)))
# [(1,2,3), (2,3,4), (3,4,5)]itertools.groupby() only groups consecutive equal elements — it does not group all elements with the same key. If the input is [A, A, B, A], you get three groups, not two. Sort the input by key first if you want all same-key elements together.itertools functions return lazy iterators — they do not materialise results. itertools.chain(a, b) returns an iterator; nothing is computed until you iterate. Wrap in list() to materialise, but only when you need all values — lazy is memory-efficient for large sequences.itertools.product() is equivalent to nested for loops, not zip. product([1,2],[3,4]) gives (1,3),(1,4),(2,3),(2,4) — all 4 combinations. zip([1,2],[3,4]) gives (1,3),(2,4) — paired by position.✅ Intermediate tab complete — check your understanding
- I know the difference between combinations (order doesn't matter) and permutations (order matters)
- I know that groupby only groups consecutive equal elements — data must be sorted first
- I can use accumulate() to create a running total without writing a loop
Infinite iterators and the iterator algebra
What you'll learn: Infinite iterators (cycle, repeat, count) for lazy generation. The concept of iterator algebra — composing simple iterators into complex transformations. The official itertools recipes.
How to read this tab: Always pair infinite iterators with islice() or takewhile() — otherwise they run forever.
The power of itertools is composition. Instead of loading everything into memory, you compose lazy iterators: sum(x*x for x in islice(count(1), 1000000)) computes the sum of the first million squares without building a list. Memory use stays constant regardless of input size.
itertools and the iterator algebra
The itertools documentation describes the module as "a set of fast, memory efficient tools that are useful by themselves or in combination" and calls the patterns from the module "iterator algebra." Every itertools function returns a lazy iterator that consumes its inputs one element at a time — composing them builds pipelines that never materialise large intermediate lists. This model is equivalent to Haskell's lazy list combinators and inspired by Clojure's transducers. The Python docs include a recipes section (docs.python.org/3/library/itertools.html#itertools-recipes) with implementations of pairwise, batched, sliding_window, and ~20 other common patterns.
✅ Expert tab complete
- I can use itertools.cycle() with islice() to create a repeating sequence of finite length
- I understand iterator algebra: composing simple iterators avoids intermediate lists