combinations
lists
list-processing
duplicates
programming

All combinations of a list of lists

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When people ask for "all combinations" of a list of lists, they usually mean the Cartesian product: pick one element from each inner list and emit every valid row. This appears in test generation, search problems, configuration expansion, and reporting. The main design choice is whether you want a simple in-memory result or a lazy iterator that can stop early.

Recognizing the Cartesian Product

Suppose you start with three groups:

python
1groups = [
2    ["red", "blue"],
3    ["S", "M"],
4    ["cotton", "wool"],
5]

The desired rows are:

python
1[
2    ("red", "S", "cotton"),
3    ("red", "S", "wool"),
4    ("red", "M", "cotton"),
5    ("red", "M", "wool"),
6    ("blue", "S", "cotton"),
7    ("blue", "S", "wool"),
8    ("blue", "M", "cotton"),
9    ("blue", "M", "wool"),
10]

This is not the same as choosing subsets from one list. Position matters. The first item must come from the first inner list, the second item from the second inner list, and so on.

The Standard Library Solution

In Python, the direct answer is itertools.product. It already handles the nested-loop logic for you and reads exactly like the problem statement.

python
1from itertools import product
2
3groups = [
4    ["red", "blue"],
5    ["S", "M"],
6    ["cotton", "wool"],
7]
8
9for combo in product(*groups):
10    print(combo)

Why this is usually the right solution:

  • It is concise and already tested.
  • It works with any iterable, not just lists.
  • It is lazy until you force materialization with list(...).

If later code wants lists rather than tuples, convert only at the boundary:

python
1from itertools import product
2
3groups = [[1, 2], [10, 20], [100, 200]]
4rows = [list(combo) for combo in product(*groups)]
5print(rows)

That keeps the generation step simple while still matching an API that expects mutable sequences.

Writing It Manually

You do not need a custom implementation most of the time, but it is useful to understand how the product is built. A recursive version is small and clear:

python
1def cartesian_product(groups):
2    if not groups:
3        return [[]]
4
5    head = groups[0]
6    tail_rows = cartesian_product(groups[1:])
7
8    result = []
9    for item in head:
10        for tail in tail_rows:
11            result.append([item] + tail)
12
13    return result
14
15
16groups = [["A", "B"], [1, 2], ["x", "y"]]
17print(cartesian_product(groups))

This version makes one important idea obvious: each result row is built by prepending one choice from the current list to every row produced by the rest of the lists.

Manual code becomes useful when you need custom behavior such as:

  • skipping invalid partial rows early
  • logging how combinations are built
  • yielding domain objects instead of tuples

Prefer Lazy Generation for Large Inputs

The output size grows multiplicatively. If your input lengths are a, b, and c, the result count is a * b * c. That means a small increase in each inner list can create a very large result.

For example, 100 * 100 * 100 already produces one million rows. In that case, generating rows lazily matters:

python
1from itertools import product
2
3for combo in product(range(1000), range(1000)):
4    if combo == (3, 5):
5        print("found", combo)
6        break

This runs without allocating one million pairs in advance. If you only need to search, stream, or write results to a file, laziness is the correct default.

You can also write a lazy recursive generator:

python
1def iter_product(groups, prefix=None):
2    prefix = prefix or []
3    if not groups:
4        yield tuple(prefix)
5        return
6
7    for item in groups[0]:
8        yield from iter_product(groups[1:], prefix + [item])
9
10
11for row in iter_product([["on", "off"], ["us", "eu"]]):
12    print(row)

Edge Cases That Matter

Two edge cases should be part of the function contract.

First, if any inner list is empty, there are no valid rows:

python
1from itertools import product
2
3groups = [[1, 2], [], ["x", "y"]]
4print(list(product(*groups)))

That prints an empty list because one missing choice eliminates every full combination.

Second, an empty outer list usually returns one empty row in product-style logic. That behavior can surprise people, but it is consistent with recursion and with the identity element of multiplication. If your application wants a different meaning, document it explicitly.

Common Pitfalls

  • Reimplementing the product with deeply nested hardcoded loops. Fix it by using itertools.product unless you need custom pruning.
  • Materializing every row with list(product(...)) when the caller only needs to iterate once. Keep the iterator lazy when possible.
  • Confusing Cartesian product with subset combinations. If one value must come from each inner list, the problem is product, not itertools.combinations.
  • Ignoring empty inner lists. One empty source means zero complete rows, so test that case directly.
  • Returning tuples to code that later mutates the rows. Convert at the edge with [list(row) for row in product(...)].

Summary

  • A list of lists problem like this is usually a Cartesian product.
  • 'itertools.product is the idiomatic Python solution.'
  • A recursive version helps when you need custom generation rules.
  • Output size grows multiplicatively, so lazy iteration is important.
  • Decide up front how to handle empty inner lists and output type.

Course illustration
Course illustration

All Rights Reserved.