Skip to content

How to Generate All Combinations from Multiple Lists in Python

Learn how to efficiently generate Cartesian products from multiple Python lists using itertools.product, handle dynamic input sizes with unpacking, and optimize memory usage for large combination spaces.

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:

from itertools import product

colors = ["Red", "Blue"]
sizes = ["S", "M", "L"]

result_product = list(product(colors, sizes))

result_loops = [(c, s) for c in colors for s in sizes]

print(result_product == result_loops)   

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
]

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]

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:

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"]

combo_iterator = product(colors, sizes, materials)

for combo in combo_iterator:
    print(combo)

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

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

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.

Navigation