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:
The desired rows are:
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.
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:
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:
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:
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:
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:
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.productunless 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.productis 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.

