How to Generate All Combinations from Multiple Lists in Python
A common programming challenge is generating every possible combination by selecting one item from each of several different lists. In mathematics, this operation is known as the Cartesian product.
You might encounter this when:
- Generating test cases for every browser, OS, and version combination.
- Creating product variations across color, size, and material.
- Exploring all possible configurations in a search space.
While you could write nested loops, Python provides a standard library tool that is cleaner, faster, and handles any number of lists automatically.
In this guide, you will learn how to use itertools.product to generate combinations from a fixed or dynamic number of lists, understand why it outperforms nested loops, and handle memory efficiently when dealing with large combination spaces.
The Standard Solution: itertools.product
The itertools module is Python's standard library for efficient iteration. The product() function computes the Cartesian product of the input iterables, producing every possible combination of one element from each input:
from itertools import product
colors = ["Red", "Blue"]
sizes = ["S", "M", "L"]
combinations = list(product(colors, sizes))
print(combinations)
Output:
[('Red', 'S'), ('Red', 'M'), ('Red', 'L'), ('Blue', 'S'), ('Blue', 'M'), ('Blue', 'L')]
Each tuple in the result contains one element from colors and one from sizes. The total number of combinations is 2 * 3 = 6.
This is functionally equivalent to nested for loops, but expressed in a single line:
# These produce identical results
from itertools import product
colors = ["Red", "Blue"]
sizes = ["S", "M", "L"]
# Using itertools.product
result_product = list(product(colors, sizes))
# Using nested loops
result_loops = [(c, s) for c in colors for s in sizes]
print(result_product == result_loops)
# Output: True
Handling a Variable Number of Lists
In real-world applications, you often receive a list of lists where the number of dimensions is not known until runtime. You cannot write hardcoded nested loops for this scenario. The unpacking operator (*) solves this by expanding a list into separate arguments:
from itertools import product
options = [
["Burger", "Sandwich"], # Main
["Fries", "Salad"], # Side
["Coke", "Water"] # Drink
]
# The * operator unpacks into: product(options[0], options[1], options[2])
all_combos = list(product(*options))
for combo in all_combos:
print(combo)
Output:
('Burger', 'Fries', 'Coke')
('Burger', 'Fries', 'Water')
('Burger', 'Salad', 'Coke')
('Burger', 'Salad', 'Water')
('Sandwich', 'Fries', 'Coke')
('Sandwich', 'Fries', 'Water')
('Sandwich', 'Salad', 'Coke')
('Sandwich', 'Salad', 'Water')
The *options syntax works regardless of how many inner lists options contains. If a fourth category like ["Cake", "Pie"] is added, the code does not need to change at all.
Using the repeat Parameter
When you need the Cartesian product of a single iterable with itself, use the repeat parameter instead of duplicating the list:
from itertools import product
digits = [0, 1]
# All 3-digit binary combinations
binary_combos = list(product(digits, repeat=3))
for combo in binary_combos:
print(combo)
Output:
(0, 0, 0)
(0, 0, 1)
(0, 1, 0)
(0, 1, 1)
(1, 0, 0)
(1, 0, 1)
(1, 1, 0)
(1, 1, 1)
This is equivalent to product(digits, digits, digits) but much cleaner, especially when the repeat count is determined at runtime.
Why Not Use Nested Loops?
For a fixed number of lists (exactly two or three), nested loops work fine. However, they have a fundamental scalability problem.
The Problem with Loops
If you want to add a fourth category to a combination generator, you must rewrite the code to add another indentation level. If the number of lists is dynamic, hardcoded loops are impossible to write:
# Adding a new category requires restructuring the entire loop
for main in mains:
for side in sides:
for drink in drinks:
for dessert in desserts: # New loop level needed
print(main, side, drink, dessert)
With itertools.product, adding a new list requires zero code changes:
from itertools import product
options = [mains, sides, drinks, desserts] # Just add to the list
for combo in product(*options):
print(combo)
| Feature | Nested Loops | itertools.product |
|---|---|---|
| Readability | Poor (deep indentation) | Excellent (flat structure) |
| Scalability | Rigid (fixed depth) | Flexible (any depth) |
| Performance | Slower (Python interpreter) | Faster (C implementation) |
| Dynamic depth | Impossible | Built-in via * unpacking |
Memory Efficiency with Lazy Evaluation
One of the greatest advantages of itertools.product is that it returns an iterator, not a list. Values are generated one at a time, only when requested:
from itertools import product
colors = ["Red", "Blue", "Green"]
sizes = ["S", "M", "L", "XL"]
materials = ["Cotton", "Polyester", "Silk"]
# This does NOT create all 36 combinations in memory at once
combo_iterator = product(colors, sizes, materials)
# Each combination is generated only when the loop requests it
for combo in combo_iterator:
print(combo)
# Process one at a time
Output:
('Red', 'S', 'Cotton')
('Red', 'S', 'Polyester')
('Red', 'S', 'Silk')
('Red', 'M', 'Cotton')
('Red', 'M', 'Polyester')
('Red', 'M', 'Silk')
('Red', 'L', 'Cotton')
('Red', 'L', 'Polyester')
('Red', 'L', 'Silk')
('Red', 'XL', 'Cotton')
('Red', 'XL', 'Polyester')
('Red', 'XL', 'Silk')
('Blue', 'S', 'Cotton')
('Blue', 'S', 'Polyester')
('Blue', 'S', 'Silk')
('Blue', 'M', 'Cotton')
('Blue', 'M', 'Polyester')
('Blue', 'M', 'Silk')
('Blue', 'L', 'Cotton')
('Blue', 'L', 'Polyester')
('Blue', 'L', 'Silk')
('Blue', 'XL', 'Cotton')
('Blue', 'XL', 'Polyester')
('Blue', 'XL', 'Silk')
('Green', 'S', 'Cotton')
('Green', 'S', 'Polyester')
('Green', 'S', 'Silk')
('Green', 'M', 'Cotton')
('Green', 'M', 'Polyester')
('Green', 'M', 'Silk')
('Green', 'L', 'Cotton')
('Green', 'L', 'Polyester')
('Green', 'L', 'Silk')
('Green', 'XL', 'Cotton')
('Green', 'XL', 'Polyester')
('Green', 'XL', 'Silk')
This distinction becomes critical with large combination spaces. If you have 10 lists with 10 items each, the result contains 10 billion combinations. Creating a list would exhaust your system's memory, but iterating lazily is perfectly safe:
from itertools import product
# 10 billion combinations - do NOT convert to list()
large_lists = [range(10) for _ in range(10)]
count = 0
for combo in product(*large_lists):
count += 1
if count >= 5:
break
print(f"Stopped after {count} combinations")
Output:
Stopped after 5 combinations
Only convert the result to a list() if you need to store all combinations in memory or access them by index. For processing, filtering, or searching, iterate directly over the product() object to keep memory usage constant.
Practical Example: Generating Test Configurations
A realistic use case is generating all test configurations for a web application:
from itertools import product
browsers = ["Chrome", "Firefox", "Safari"]
operating_systems = ["Windows", "macOS", "Linux"]
screen_sizes = ["1920x1080", "1366x768"]
test_configs = product(browsers, operating_systems, screen_sizes)
print(f"Total test cases: {len(browsers) * len(operating_systems) * len(screen_sizes)}")
print()
for i, (browser, os, screen) in enumerate(test_configs, 1):
print(f"Test {i:2d}: {browser} on {os} at {screen}")
Output:
Total test cases: 18
Test 1: Chrome on Windows at 1920x1080
Test 2: Chrome on Windows at 1366x768
Test 3: Chrome on macOS at 1920x1080
Test 4: Chrome on macOS at 1366x768
Test 5: Chrome on Linux at 1920x1080
Test 6: Chrome on Linux at 1366x768
Test 7: Firefox on Windows at 1920x1080
Test 8: Firefox on Windows at 1366x768
Test 9: Firefox on macOS at 1920x1080
Test 10: Firefox on macOS at 1366x768
Test 11: Firefox on Linux at 1920x1080
Test 12: Firefox on Linux at 1366x768
Test 13: Safari on Windows at 1920x1080
Test 14: Safari on Windows at 1366x768
Test 15: Safari on macOS at 1920x1080
Test 16: Safari on macOS at 1366x768
Test 17: Safari on Linux at 1920x1080
Test 18: Safari on Linux at 1366x768
Conclusion
To generate all combinations from multiple lists in Python, use itertools.product.
- Pass your lists as separate arguments for a fixed number of inputs, or use the
*unpacking operator to handle a dynamic list of lists. - Iterate directly over the result for memory-efficient processing, and only convert to a
list()when you need random access to all combinations.
This approach is cleaner, faster, and more scalable than nested loops, and it handles any number of input lists without code changes.