Learn by reading through in order

itertools and functools — Iteration and Function Composition Toolkits

Learn combinations / product / chain / accumulate iteration recipes, fixing arguments with partial, and speeding recursion via @lru_cache memoization through examples.

itertools is a set of functions that build new iterables from existing ones (combinations, concatenation, accumulation), and functools is a set of functions that transform the function itself (argument binding, memoization). Both can replace for loop + state-tracking patterns in a single line, dramatically shrinking your code.

Iterables — values you can loop over

Most itertools functions take an "iterable" as input and return a new iterable, so it's worth pinning down what an iterable is first. An iterable (the thing you can write to the right of for x in ___:) is anything you can pass to a for loop, list(...), sum(...), or map(...). list / tuple / str / dict / set / range, plus generators you write yourself (functions using yield), all behave as iterables in Python.

Common iterables
list[1, 2, 3]tuple(1, 2, 3)str"abc"dict{"a": 1}set{1, 2, 3}rangerange(5)
The thing you can put after for x in is an iterable. Built-in types like list / tuple / str / dict / set / range are iterables, and so are custom generators built with yield.

itertools — combination and iteration toolkit

itertools is a module gathering functions that build new iterables from existing ones. The four you'll use most are combinations (unordered combinations), product (Cartesian product over multiple sets), chain (concatenate iterables into one), and accumulate (running totals or running products). Knowing these four lets you rewrite double for loops and state-tracking loops much more cleanly.

The four core itertools functions
combinationsUnordered combinationsproductAll-pairs across setschainConcatenate iterablesaccumulateRunning sum / product
combinations for unordered pairs / triples, product for all-pairs across sets, chain for concatenating iterables, and accumulate for running state while scanning. All four return iterators, so wrap with list(...) to materialize.
FunctionInput → OutputExample
combinations(it, k)k-element combinations (unordered)[A,B,C] → (A,B), (A,C), (B,C)
permutations(it, k)k-element permutations (ordered)[A,B] → (A,B), (B,A)
product(*its)Cartesian product across sets[S,M] × [red,blue] → 4 combos
chain(*its)Concatenate multiple iterables[1,2] + [3,4] → [1,2,3,4]
accumulate(it)Accumulate from the left (sum by default)[10,20,30] → [10, 30, 60]

List pairs of two drinks chosen from three options and all size × color SKUs. combinations gives unordered pairs, product gives the Cartesian product across sets.

① Import combinations and product from itertools

② From the drinks ["coffee", "tea", "juice"], build pairs of 2 with combinations, materialize, and print as Pairs: ◯

③ Build the all-combos of sizes ["S", "M", "L"] × colors ["red", "blue"] with product, materialize, and print as All SKUs: ◯

(If your code runs correctly, the explanation will appear.)

Python Editor

Run code to see output

Concatenate two days of sales data and compute the running total. chain stitches multiple iterables into one and accumulate scans from the left, carrying a running value.

① Import chain and accumulate from itertools

② Concatenate day 1 sales [120, 80] and day 2 sales [200, 150, 90] with chain, materialize, and print as All sales: ◯

③ Apply accumulate to the concatenated result, materialize, and print as Running total: ◯

Python Editor

Run code to see output

functools.partial — bind arguments ahead of time

functools.partial builds a new function with some arguments already locked in. When you need to pass a function with bound arguments to a callback or higher-order function, this saves you from writing one-line wrapper functions like def wrapper(...): .... Anywhere you call the same function repeatedly with slight argument variations, partial cuts wrapper noise and makes the code more readable.

from functools import partial

def format_with_unit(price, unit):
    return f"{price}{unit}"

# Build a new function with the unit "USD" pre-bound
to_usd = partial(format_with_unit, unit="USD")

# Apply to individual prices — only price needs to be passed now
print(to_usd(100))   # 100USD
print(to_usd(200))   # 200USD
print(to_usd(300))   # 300USD
How partial works
power(base, exp)Returns base ** exppartial(power, exp=2)Locks exp=2square(3) → 9square(5) → 25
partial(function, arg=value) returns a new function with some arguments locked in. Lock exp=2 on power(base, exp) and you get a square function that just needs base.

Use partial to lock exp on power(base, exp) and derive dedicated square / cube functions.

① Import partial from functools

② Define def power(base, exp): that returns `base ** exp

③ With partial, build a `square` function with `exp=2` locked. Call square(3) and square(5) and print as 3 squared: ◯ / 5 squared: ◯

④ With partial, build a `cube` function with `exp=3` locked. Call cube(2) and print as 2 cubed: ◯

Python Editor

Run code to see output

functools.lru_cache — cache results with memoization

"I want to reuse the result of an expensive function called repeatedly with the same arguments" — comes up whenever a function's outputs are determined by its inputs and speed matters. Hand-rolling this means managing your own cache dict, but @lru_cache does it in a single decorator line.

@lru_cache is a decorator that caches a function's return values. When called again with the same arguments, it returns the cached value directly, skipping the recomputation. LRU (Least Recently Used — discards the entries unused for the longest) drops old cache entries so you can cap memory usage with something like maxsize=128.

How lru_cache works
fib(30) first timeRun bodyCache result+ return valuefib(30) second timeRead from cache→ Instant return
The first call misses the cache, so the function body runs and the result is stored as (args → return value). Subsequent calls with the same args return immediately from the cache — the body never runs.

Don't add to functions with side effects

@lru_cache assumes "same arguments → same return value". Apply it to functions with side effects (reading files / writing to a DB / returning the current time) or whose result varies for the same inputs, and you'll have a bug where the first result keeps coming back forever. Apply only to pure functions (same input → same output, no side effects).

Speed up a recursive Fibonacci with @lru_cache. Even at fib(30) the call count is huge without caching, but with the decorator it's instant.

① Import lru_cache from functools

② Define a fib(n) function decorated with @lru_cache(maxsize=128) (return n if n < 2, otherwise return fib(n-1) + fib(n-2))

③ Print fib(30) as fib(30): ◯

④ Print fib(50) as fib(50): ◯ (without caching, this wouldn't finish in any reasonable time)

⑤ Print maxsize: ◯ using fib.cache_info().maxsize

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What's the most concise way to build the Cartesian product of [1, 2, 3] and [A, B]?

Q2What does partial(f, x=10) return?

Q3Why does @lru_cache speed up a naive recursive Fibonacci?