skip to content

itertools Patterns

The standard library's lazy-iteration toolbox: compose chain, islice, accumulate and the combinatorial generators instead of building intermediate lists. Naming the right tool is the question.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

15

What is the difference between itertools.permutations and itertools.combinations?

level: juniorimportance: must knowfreq 62%

answer

  1. One cares about arrangement, one does not
  2. Three letters, pairs: six versus three
  3. r! permutations per single combination
  4. math.perm and math.comb count without iterating
  5. Selection is positional, never by value

basics

~20 s

itertools.permutations treats order as significant, so ('a','b') and ('b','a') are both yielded. itertools.combinations yields each selection once, keeping the input's own order. For n items taken r at a time that is n!/(n-r)! tuples versus n!/(r!(n-r)!).

solid answer

~40 s

Both take an iterable and a length `r` and yield tuples lazily, but they differ on whether arrangement counts. `itertools.permutations('abc', 2)` yields six tuples, including both `('a','b')` and `('b','a')`, because it enumerates ordered arrangements. `itertools.combinations('abc', 2)` yields three — `('a','b')`, `('a','c')`, `('b','c')` — one per unordered selection, always emitting the chosen elements in the order they appeared in the input. The counts are the familiar formulas: n!/(n-r)! for permutations and n!/(r!(n-r)!) for combinations, computable without running anything via `math.perm` and `math.comb`. Both select by position rather than by value, so neither deduplicates equal elements. Reach for combinations when the result is a set-like choice (which two servers to drain) and permutations when the arrangement itself is the answer (in which order to visit them).

code

python · 5 lines
python
from itertools import permutations, combinations

items = ["a", "b", "c"]
print(list(permutations(items, 2)))
print(list(combinations(items, 2)))

go deeper

for a junior

Recall that permutations counts orderings and combinations does not, and be able to write both calls for a three-letter string and say how many tuples each returns.

for a middle

Explain the two count formulas, that output order mirrors input order rather than being sorted, and that both select by position so equal elements produce duplicate tuples.

for a senior

Show the instinct to size the enumeration with math.perm or math.comb before iterating, and explain that laziness bounds memory but never runtime.

for a principal

Frame the choice as a modelling decision: picking permutations where combinations was meant multiplies the work by r! and silently returns rearrangements as if they were distinct results.

### The two questions they answer Every combinatorial enumeration starts with one decision: **does the arrangement of the chosen elements matter?** `itertools.permutations` says yes; `itertools.combinations` says no. Everything else about the two functions — their signatures, their laziness, their ordering, their output counts — follows from that single distinction. Both have the shape `f(iterable, r=None)`. They pull the whole input into an internal tuple up front, then yield tuples of length `r` one at a time. For `permutations`, `r` defaults to the full length of the input; `combinations` requires `r` explicitly. ```python from itertools import permutations, combinations items = ['a', 'b', 'c'] list(permutations(items, 2)) # [('a','b'), ('a','c'), ('b','a'), ('b','c'), ('c','a'), ('c','b')] list(combinations(items, 2)) # [('a','b'), ('a','c'), ('b','c')] ``` The permutation list contains three extra tuples: each combination appears in both of its orderings. In general each combination of size r corresponds to r! permutations, which is exactly why the two count formulas differ by that factor. ### The counts, and why they matter more than the API For n input elements taken r at a time: * permutations: n! / (n - r)! — the number of ordered arrangements. * combinations: n! / (r! (n - r)!) — the binomial coefficient, usually written C(n, r). The standard library will compute both for you without generating anything: `math.perm(n, r)` and `math.comb(n, r)`. This is the habit worth forming — before writing a loop over one of these iterators, evaluate its size. `math.comb(52, 5)` is 2,598,960, a fine thing to iterate. `math.perm(20, 20)`, which is 20!, is about 2.4 x 10^18, and no amount of laziness makes that loop terminate. ### Ordering of the output Neither function sorts. Both walk index positions of the internal input tuple in increasing order, so the output order is a function of **input order**, not of value. If the input is already sorted, the output tuples come out in lexicographic order — which is why textbook examples always look sorted. Feed unsorted input and you get unsorted output, in a completely deterministic order that mirrors the input's own sequence. A direct consequence: **selection is positional, not value-based.** If the input contains equal elements, the output contains equal tuples. `combinations(['a','a','b'], 2)` yields `('a','a')`, `('a','b')` and `('a','b')` — two identical pairs, because the two `'a'` values sit at different positions. Neither function has any notion of a set. ### Laziness and memory Both return iterators, so nothing is precomputed: memory is the input tuple plus one output tuple plus a small array of indices. That means the memory cost is trivial even for astronomically large enumerations — and it is precisely why the runtime trap is so easy to fall into. Laziness bounds memory, never time. A `for` loop over `permutations` of twenty things will still be running long after everyone has gone home. Because the input is drained into a tuple immediately, passing a generator to either function consumes it in full before the first tuple is yielded, and passing an endless iterator hangs at construction rather than at the first `next()`. ### Choosing between them The test is a sentence: read the problem statement and ask whether swapping two chosen elements produces a different answer. * Which two of these five fee codes appear together on the same invoice? Order is meaningless — `combinations`. * In what sequence do we apply three discounts to a line item? Order changes the total — `permutations`. * Every possible assignment of one of three tiers to each of four accounts? That is neither: it is a Cartesian product, `itertools.product` with `repeat=4`. Getting this wrong is a silent bug, not a crash: an enumeration using `permutations` where `combinations` was meant simply does r! times the work and reports duplicated results that differ only in arrangement, which is easy to miss when the downstream code aggregates. ### Interview framing Interviewers ask this as a screen because it merges three things a working engineer needs: knowing the stdlib exists rather than hand-rolling recursion, recalling the two counting formulas, and having the instinct to estimate the output size before iterating it. A complete answer names both functions, shows a three-element example, states both formulas, and adds that the output order follows input order and that neither deduplicates.

  • How many tuples does itertools.permutations(range(10), 3) yield, and how would you work that out without running it?
    720 — that is 10!/(10-3)! = 10 x 9 x 8. `math.perm(10, 3)` returns it directly, and `math.comb(10, 3)` returns 120, the same enumeration with order ignored; the ratio is 3! = 6. Computing the count first is the habit that stops an enumeration from being started and then abandoned.
  • What does itertools.permutations do when you omit the second argument?
    `r` defaults to the full length of the input, so it yields every full-length arrangement — n! tuples. That default is a common accident: `permutations(items)` on a ten-element list is 3,628,800 tuples, while the author usually meant pairs. `itertools.combinations` has no such default and requires `r`.
  • Do either of these functions remove duplicate results when the input contains equal elements?
    No. Both select by position, not by value, so equal input elements produce equal output tuples. `combinations(['a','a','b'], 2)` yields `('a','b')` twice. If you need distinct results, deduplicate the input first, or collect the output into a set — accepting that the enumeration still does the full amount of work.

Combinations are who you invite to dinner; permutations are where each guest sits. The same guest list seats many different ways.

saying these in an interview costs you the question

  • Says combinations sorts its output regardless of input order
  • Claims either function deduplicates equal input elements
  • Thinks permutations and combinations yield the same number of tuples
  • Believes laziness makes a factorial enumeration cheap to run
  • Confuses combinations with the Cartesian product of several iterables
  • Cannot state either count formula even approximately

context

open as a page

Why does itertools.groupby split one key into several groups on unsorted input?

level: juniorimportance: must knowfreq 38%

basics

~20 s

itertools.groupby groups only consecutive items whose computed key is equal, and it never sorts. On unsorted input the same key reappears later and starts a fresh group, so sort by the same key function first.

open as a page

How do you bound itertools.count(), cycle() and repeat() so a loop terminates?

level: juniorimportance: must knowfreq 60%

basics

~10 s

itertools.count, cycle and repeat never raise StopIteration, so draining one runs forever. The consumer has to stop them: wrap with itertools.islice or itertools.takewhile, zip against a finite iterable, or break out of the loop.

open as a page

How do itertools.chain and itertools.chain.from_iterable differ when flattening a list of lists?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both yield the items of the inner iterables one level flatter, lazily. itertools.chain(*rows) unpacks the outer sequence into arguments first, so it must be finite and in memory; chain.from_iterable(rows) takes the outer iterable itself and pulls it lazily.

open as a page

How does itertools.zip_longest differ from the built-in zip on unequal-length inputs?

level: juniorimportance: should knowfreq 48%

basics

~10 s

The built-in zip stops at the shortest input and silently discards the rest. itertools.zip_longest runs until the longest input is exhausted and substitutes fillvalue, which defaults to None, for every missing element.

open as a page

How does itertools.product with repeat= replace nested for loops in Python?

level: middleimportance: should knowfreq 52%

basics

~20 s

itertools.product yields the Cartesian product of its input iterables as tuples, exactly as nested for loops would, with the rightmost position varying fastest. repeat=n means 'use these same iterables n times', so product(x, repeat=3) equals three loops over x.

open as a page

Why can itertools.combinations return duplicate tuples from one input list?

level: middleimportance: should knowfreq 38%

basics

~20 s

itertools.combinations selects by index position, not by value, so two equal elements at different positions produce two equal output tuples. Its output order likewise follows the input's order — it is lexicographic only when the input is already sorted.

open as a page

How does itertools.accumulate with a custom binary function differ from functools.reduce?

level: middleimportance: should knowfreq 34%

basics

~10 s

itertools.accumulate folds a binary function over the input and yields every intermediate result, one per item, lazily. functools.reduce performs the same left fold but returns only the final value. accumulate defaults to addition.

open as a page

Why is an itertools.groupby sub-iterator empty after the outer loop advances?

level: middleimportance: should knowfreq 42%

basics

~20 s

All groups share one underlying source iterator. Advancing the outer groupby object invalidates the previous group and skips its unread items, so a group read later yields nothing. Consume each group inside the loop, typically with list().

open as a page

What is the difference between itertools.takewhile and itertools.dropwhile?

level: middleimportance: should knowfreq 36%

basics

~10 s

itertools.takewhile yields items until the predicate first returns false and then stops permanently. itertools.dropwhile discards items while the predicate is true, then yields the first false item and everything after it without testing again.

open as a page

What does itertools.islice consume, and why does it reject negative indices?

level: middleimportance: should knowfreq 40%

basics

~20 s

itertools.islice pulls items from the underlying iterator and cannot rewind, so it consumes and discards everything it skips and leaves the source positioned after the last item it yielded. Negative start, stop or step would require knowing the length, so they raise ValueError.

open as a page

How do you size an itertools.product space before a nightly billing run enumerates it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Compute the output count before iterating: math.prod of the input lengths for itertools.product, n**k when repeat=k, math.comb for combinations and math.perm for permutations. Laziness bounds memory, never runtime, so the count is the only early warning.

open as a page

How much memory can itertools.tee hold when one forked iterator runs far ahead of another?

level: seniorimportance: should knowfreq 38%

basics

~20 s

itertools.tee holds every item until the slowest of its forked iterators has consumed it. If one fork is drained before the other starts, the buffer grows to the entire stream, and a plain list is then cheaper and clearer.

open as a page

When does a collections.defaultdict accumulation beat itertools.groupby for grouping?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Whenever the input is not already ordered by the group key. A defaultdict(list) pass is one O(n) sweep with no sort, tolerates unorderable keys and preserves first-seen order. Reach for itertools.groupby when the source already arrives sorted.

open as a page

How does itertools.batched chunk a lazy record iterator, and what happens at the tail?

level: seniorimportance: should knowfreq 30%

basics

~10 s

itertools.batched, added in 3.12, pulls lazily from an iterable and yields tuples of length n; the final tuple may be shorter. Since 3.13, passing strict=True makes an incomplete final batch raise ValueError instead.

open as a page