Back to Blog
Python

Python itertools product: Cartesian Products Explained

python itertools product: Learn how to use itertools.product in Python to compute Cartesian products, replace nested loops, and handle large combination spaces with la...

itertoolsCartesian productPython standard librarygeneratorsnested loopsiteration
A diagram showing three input lists merging into a grid of tuple combinations, representing the Cartesian product produced by itertools.product.

python itertools product requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The itertools.product function in Python computes the Cartesian product of two or more iterables. It lives in the itertools module of the standard library and is imported with from itertools import product. The function returns an iterator that yields tuples, each tuple containing one element from each input iterable.

from itertools import product colors = ["red", "green", "blue"] sizes = ["S", "M", "L"] for combo in product(colors, sizes): print(combo)

This prints every color and size pair in a deterministic order: the first iterable changes slowest and the last changes fastest. That ordering mirrors nested for loops where the rightmost loop iterates most frequently, which is worth remembering when the output order matters downstream.

Understanding the repeat Parameter

The repeat parameter lets you pass the same iterable multiple times without repeating it in the argument list. product(digits, repeat=3) is equivalent to product(digits, digits, digits).

from itertools import product digits = [0, 1] for combo in product(digits, repeat=3): print(combo)

This yields all 8 binary triples: (0, 0, 0) through (1, 1, 1). The repeat value must be a non-negative integer. When repeat=0, the function yields a single empty tuple (), matching the mathematical definition of a zero-argument Cartesian product. This edge case rarely appears in practice, but it explains why the function behaves consistently across all valid inputs.

Replacing Nested Loops

A common reason to reach for product is replacing deeply nested for loops. Configuration testing is a typical scenario: you need every combination of environment, database, and cache backend.

from itertools import product environments = ["dev", "staging", "prod"] databases = ["postgres", "mysql"] cache_backends = ["redis", "memcached"] for env, db, cache in product(environments, databases, cache_backends): run_test(env, db, cache)

The nested-loop equivalent requires three levels of indentation and becomes harder to read as the number of iterables grows. product flattens the iteration into a single loop and makes the variable unpacking explicit. The same pattern scales to five or ten iterables without adding indentation depth.

One behavior to keep in mind: product consumes its input iterables eagerly at call time. If an input is a generator, it is fully exhausted when product is constructed, not when the first tuple is requested. The output iterator is lazy, but the inputs are materialized internally. This differs from zip, which also consumes lazily, and it matters when you pass a generator that has already been partially consumed.

Memory Behavior and Performance

product does not precompute the full Cartesian product in memory. It produces tuples one at a time, so the memory footprint is proportional to the number of input iterables, not the total number of combinations. This is the main advantage over building a list of all combinations with a comprehension.

# Materializes everything at once all_combos = [(a, b, c) for a in A for b in B for c in C] # Lazy: one tuple at a time for combo in product(A, B, C): process(combo)

The total number of combinations is the product of the lengths of the inputs. Three iterables of length 10 produce 1,000 tuples; ten iterables of length 10 produce 10 billion. The lazy iterator avoids allocating all 10 billion tuples at once, but the loop still runs proportionally long. The runtime cost is inherent to the size of the combination space, not to product itself.

When the combination space is small and the results are needed repeatedly, materializing into a list is simpler and perfectly acceptable. When the space grows or the processing is streaming, the iterator form is the better choice.

Common Mistakes and Edge Cases

Passing a Single Iterable Without repeat

product(items) yields tuples of length one, one per element. If the goal is to pair elements with themselves, repeat=2 is required.

from itertools import product items = ["a", "b", "c"] list(product(items)) # [('a',), ('b',), ('c',)] list(product(items, repeat=2)) # [('a', 'a'), ('a', 'b'), ('a', 'c'), ...]

Empty Iterables

If any input iterable is empty, product yields nothing, because no combination can be formed. This follows the mathematical definition of the Cartesian product. A frequent bug is passing an empty list and expecting the remaining iterables to still produce output.

Generator Inputs Are Consumed

Because product materializes its inputs, a partially consumed generator produces surprising results.

gen = (x for x in range(3)) next(gen) # consumes 0 list(product(gen, [1, 2])) # only sees 1 and 2 from gen

If the generator must be used from the start, pass a fresh iterator or materialize it into a list first.

Comparing product with Other Approaches

For a fixed, small number of iterables, nested loops are readable and may be preferable when the loop body needs access to the outer loop variables directly. product becomes the better choice when the number of iterables varies at runtime or when nesting depth would become excessive.

A recursive generator can produce the same Cartesian product, but it is more code and more error-prone:

def cartesian(iterables): if not iterables: yield () return for item in iterables[0]: for rest in cartesian(iterables[1:]): yield (item,) + rest

This is roughly what product does internally, but product is implemented in C and avoids Python-level recursion overhead. For nearly all cases, the standard-library version is the correct choice. Write a custom generator only when you need a fundamentally different ordering or a filtered subset of combinations.

Combining product with Other itertools Functions

product composes well with other itertools tools. A practical pattern is generating parameter combinations and mapping them through a function with starmap.

from itertools import product, starmap def configure(env, db, cache): return f"{env}-{db}-{cache}" configs = list(starmap(configure, product(environments, databases, cache_backends)))

starmap unpacks each tuple from product and passes the elements as positional arguments to the function. This keeps combination generation separate from processing logic.

Another common use is generating coordinate grids with range:

from itertools import product grid = list(product(range(5), range(5)))

This produces all (x, y) pairs for a 5x5 grid, which is useful in image processing, board-game generation, and parameter sweeps.

When product Is Not the Right Tool

If the goal is permutations or combinations rather than the full Cartesian product, itertools.permutations and itertools.combinations are the correct tools. product includes repeated elements: product("AB", repeat=2) yields ('A', 'A'), which combinations never produces.

If the iterables are extremely large and only a sample of combinations is needed, product is not helpful because it iterates in a fixed order. In that case, random sampling over a range of indices, or a custom generator, may be more appropriate.

The decision between product and alternatives comes down to three factors: whether repetition is allowed, whether the full combination space is needed, and whether the input set is known only at runtime. When repetition is allowed, the full space is required, and the inputs are dynamic, product is the standard-library tool that fits.

python itertools product: Practical Usage and Code Examples | RYUSLOG DEV