skip to content

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

level: middleimportance: should knowfreq 38%

answer

  1. It compares positions, never values
  2. Count is always the binomial coefficient
  3. Sorted output only from sorted input
  4. Dedupe the input, not the output
  5. A different function repeats one element

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.

solid answer

~40 s

`itertools.combinations` walks increasing index positions of its input; it has no notion of value equality and never deduplicates. Given `['b', 'a', 'a']` and r=2 it yields `('b','a')`, `('b','a')` and `('a','a')` — the first two are equal tuples drawn from different positions. The same positional rule explains the ordering: output follows input order, so tuples come out lexicographically sorted only if you sorted the input first. If you need distinct results, deduplicate the input before the call — that also shrinks the enumeration — or collect the output into a set, which costs the full enumeration anyway. Note that `('a','a')` here comes from two distinct equal elements; the function that pairs a single element with itself is `itertools.combinations_with_replacement`, a different tool.

code

python · 5 lines
python
from itertools import combinations, combinations_with_replacement

print(list(combinations(["b", "a", "a"], 2)))
print(list(combinations(sorted(set(["b", "a", "a"])), 2)))
print(list(combinations_with_replacement("ab", 2)))

go deeper

for a junior

Remember that itertools.combinations picks positions rather than values, so a list containing equal elements yields equal tuples and nothing filters them out.

for a middle

Explain that the output count is always the binomial coefficient of the input length, that ordering mirrors the input, and how combinations_with_replacement differs.

for a senior

Argue for deduplicating the input over the output, since one shrinks the enumeration and the other pays for it in full, and know when duplicate positions are actually meaningful.

for a principal

Set the team convention: state explicitly whether a combinatorial call operates over distinct keys or over records, because the difference is silent and only shows up in downstream aggregates.

### Positional selection is the whole answer `itertools.combinations(iterable, r)` first turns its input into a tuple, then yields tuples of elements at strictly increasing index positions: (0,1), (0,2), (1,2) and so on. It never compares two elements, never hashes one, and never sorts. Every surprising behaviour people report about it is a restatement of that one design fact. ```python from itertools import combinations list(combinations(['b', 'a', 'a'], 2)) # [('b','a'), ('b','a'), ('a','a')] ``` Three tuples come out because there are three index pairs, C(3,2) = 3. The first two are equal as values, since positions 1 and 2 hold equal elements. Nothing has gone wrong: the count is always exactly the binomial coefficient of the input length, regardless of how many elements are equal. ### Why output order looks sorted in every tutorial Because index pairs are produced in increasing order, the output order is the input's own order lifted to tuples. If the input happens to be sorted, the output is lexicographically sorted, which is why every documentation example looks tidy. Hand it unsorted input and the output is just as deterministic but no longer lexicographic: ```python list(combinations(['c', 'a', 'b'], 2)) # [('c','a'), ('c','b'), ('a','b')] ``` The practical rule: **if downstream code depends on lexicographic order, sort the input yourself.** Do not rely on the function to do it; it will not, and the bug appears only once someone changes how the input list is built. The same reasoning applies to `itertools.permutations` and to `itertools.combinations_with_replacement`, which are built from the same positional machinery. ### Getting distinct results There are two honest strategies, and they differ in cost. 1. **Deduplicate the input first.** `combinations(sorted(set(items)), r)` produces distinct tuples in lexicographic order, and — crucially — it *shrinks the enumeration*: fewer input elements means a smaller binomial coefficient, so you do less work, not just less output. 2. **Deduplicate the output.** `set(combinations(items, r))` also produces distinct tuples, but it pays for the entire original enumeration first and then discards the surplus. It is the right choice only when the tuples must reflect distinct *positions* for some other reason, or when the input contains unhashable elements that cannot be put in a set beforehand. When elements are equal in value but carry distinguishing identity — two invoice lines with the same amount but different ids — the duplicates are usually not duplicates at all, and deduplicating would be the bug. Decide deliberately which of the two you have. ### The neighbour that actually repeats an element A common conflation: `('a','a')` appearing in the output above does **not** mean `combinations` reuses an element. It picked two different positions that happened to hold equal values. The function that genuinely pairs an element with itself is `itertools.combinations_with_replacement`: ```python from itertools import combinations_with_replacement list(combinations_with_replacement('ab', 2)) # [('a','a'), ('a','b'), ('b','b')] ``` Here the input has two distinct elements and one output tuple still repeats `'a'`, because the selection allows non-decreasing index positions rather than strictly increasing ones. Its count is C(n + r - 1, r), not C(n, r): for n=2, r=2 that is three, not one. Reach for it when the same option may legitimately be chosen more than once — allocating three identical slots from a menu of choices, or enumerating multisets. ### Putting the four functions on one grid Two independent questions place every combinatorial function in `itertools`: * May an element be reused within one tuple? * Does the arrangement of the chosen elements matter? No/No is `combinations`. No/Yes is `permutations`. Yes/No is `combinations_with_replacement`. Yes/Yes is `product` with `repeat=`. Being able to draw that two-by-two grid on a whiteboard, with a three-element example under each cell, is the answer an interviewer is looking for when they open with a duplicates question. ### The review habit In code review, the tell is a call to `combinations` over a list built from records rather than from a deduplicated key set, followed by downstream code that assumes each result is unique. Either the duplicates are meaningful, in which case say so in a comment, or the input should have been reduced to distinct keys before the call — and reducing it first is both cheaper and clearer.

  • Which is better for distinct results: combinations(sorted(set(items)), r) or set(combinations(items, r))?
    Deduplicating the input is better whenever the elements are hashable. It shrinks the binomial coefficient, so you generate fewer tuples rather than generating them all and throwing some away, and the output arrives in lexicographic order for free. Deduplicating the output is the fallback for unhashable elements, or when you deliberately want position-distinct results.
  • How does itertools.combinations_with_replacement change the output count?
    It allows non-decreasing index positions instead of strictly increasing ones, so an element can appear more than once in a tuple. The count becomes C(n + r - 1, r) rather than C(n, r) — for n=5, r=3 that is 35 instead of 10. Use it for multisets: choosing r items from n kinds where repeats are legitimate.
  • Does itertools.combinations require its input to be sorted?
    No. It imposes no requirement on its input at all and simply mirrors whatever order it is given, yielding tuples of elements at increasing index positions. Sorting first is a choice you make when downstream code wants lexicographic output, or when you are deduplicating with `set` in the same expression to shrink the enumeration.

saying these in an interview costs you the question

  • Says combinations removes duplicate tuples automatically
  • Claims output is always lexicographically sorted
  • Thinks ('a','a') proves combinations reuses one element
  • Deduplicates the output when the input could be deduplicated
  • Believes combinations requires sorted input to be correct
  • Confuses combinations_with_replacement with permutations

context