Skip to main content

How to Generate Combinatoric Sequences Using itertools in Python

Python's itertools module provides four powerful functions for generating combinatoric sequences efficiently. Each one serves a distinct mathematical purpose, and choosing the right one depends on two questions: does order matter? and are repeated elements allowed?

In this guide, you will learn how each function works, understand the mathematical differences between them, and see practical examples that demonstrate when to use each one.

product() for Cartesian Products

The product() function generates every possible pairing across one or more iterables. Order matters and repetition is allowed:

from itertools import product

# Two iterables
result = list(product([1, 2], ["A", "B"]))
print(result)

Output:

[(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]

Every element from the first iterable is paired with every element from the second. For three or more iterables, the same principle extends:

from itertools import product

result = list(product("AB", "CD", "EF"))
print(f"Total combinations: {len(result)}")
print(result[:4])

Output:

Total combinations: 8
[('A', 'C', 'E'), ('A', 'C', 'F'), ('A', 'D', 'E'), ('A', 'D', 'F')]

The repeat parameter computes the Cartesian product of an iterable with itself:

from itertools import product

# All 3-bit binary numbers
binary = list(product([0, 1], repeat=3))
print(binary)

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

Common use cases: password generation, grid coordinates, exhaustive test configuration.

permutations() for Ordered Arrangements

The permutations() function generates all possible arrangements of r items from the input, where order matters but each element is used at most once:

from itertools import permutations

# All 2-item permutations from 3 elements
result = list(permutations("ABC", 2))
print(result)

Output:

[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

Notice that both ('A', 'B') and ('B', 'A') appear because order matters. When r is omitted, it defaults to the full length of the input:

from itertools import permutations

# All arrangements of 3 items
result = list(permutations("ABC"))
print(result)
print(f"Total: {len(result)}")

Output:

[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]
Total: 6

Common use cases: anagram generation, seating arrangements, route planning.

combinations() for Unordered Selections

The combinations() function generates subsets of r items where order does not matter. Each element is used at most once:

from itertools import combinations

result = list(combinations("ABCD", 2))
print(result)

Output:

[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]

Unlike permutations, ('A', 'B') and ('B', 'A') are considered the same selection, so only one appears:

from itertools import combinations

# Choose 3 from 4
result = list(combinations([1, 2, 3, 4], 3))
print(result)

Output:

[(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]

Common use cases: poker hands, team selection, lottery number combinations.

combinations_with_replacement() for Repeated Selections

The combinations_with_replacement() function works like combinations() but allows the same element to appear multiple times in each result:

from itertools import combinations_with_replacement

result = list(combinations_with_replacement("AB", 2))
print(result)

Output:

[('A', 'A'), ('A', 'B'), ('B', 'B')]

The pair ('A', 'A') is now included because repetition is allowed:

from itertools import combinations_with_replacement

result = list(combinations_with_replacement([1, 2, 3], 2))
print(result)

Output:

[(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]

Common use cases: coin combinations, dice sum analysis, multiset problems.

Visual Comparison

To see the differences clearly, here are all three selection-based functions applied to the same input with r=2:

from itertools import permutations, combinations, combinations_with_replacement

items = ["A", "B", "C"]

perms = list(permutations(items, 2))
combs = list(combinations(items, 2))
combs_rep = list(combinations_with_replacement(items, 2))

print(f"permutations ({len(perms)}): {perms}")
print(f"combinations ({len(combs)}): {combs}")
print(f"combinations_with_replacement ({len(combs_rep)}): {combs_rep}")

Output:

permutations (6):                    [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
combinations (3): [('A', 'B'), ('A', 'C'), ('B', 'C')]
combinations_with_replacement (6): [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
The Key Distinction

Permutations treat ('A', 'B') and ('B', 'A') as different results because order matters. Combinations treat them as the same selection and include only one.

Counting Results Without Generating Them

You can calculate the number of results mathematically using Python's math module, which is useful for estimating the size of a result set before generating it:

from math import comb, perm

n, r = 5, 3

print(f"product: {n ** r}")
print(f"permutations: {perm(n, r)}")
print(f"combinations: {comb(n, r)}")
print(f"combinations_with_replacement: {comb(n + r - 1, r)}")

Output:

product:                        125
permutations: 60
combinations: 10
combinations_with_replacement: 35

Practical Examples

Generating All Possible PINs

from itertools import product

pin_count = 0
for pin in product(range(10), repeat=4):
pin_count += 1

print(f"Total 4-digit PINs: {pin_count}")

Output:

Total 4-digit PINs: 10000

Finding Anagrams of a Word

from itertools import permutations

word = "cat"
anagrams = ["".join(p) for p in permutations(word)]
print(anagrams)

Output:

['cat', 'cta', 'act', 'atc', 'tca', 'tac']

Counting Lottery Combinations

from math import comb

# Choose 6 numbers from 49
total_tickets = comb(49, 6)
print(f"Possible lottery tickets: {total_tickets:,}")

Output:

Possible lottery tickets: 13,983,816

Memory Efficiency

All four functions return iterators, not lists. They generate results lazily, one at a time, with O(1) startup time. This makes them safe to use even when the total number of results is extremely large:

from itertools import product

# 10 billion combinations - perfectly safe to iterate
huge = product(range(10), repeat=10)

# Only generates values as the loop requests them
first_five = []
for i, combo in enumerate(huge):
first_five.append(combo)
if i >= 4:
break

print(first_five)

Output:

[(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0, 0, 1), (0, 0, 0, 0, 0, 0, 0, 0, 0, 2), (0, 0, 0, 0, 0, 0, 0, 0, 0, 3), (0, 0, 0, 0, 0, 0, 0, 0, 0, 4)]
warning

Avoid calling list() on large combinatoric results. Converting 10 billion tuples to a list would exhaust your system's memory. Iterate directly over the iterator instead.

Quick Reference

FunctionOrder MattersRepeats AllowedFormulaExample Use Case
productYesYesn^rPasswords, grids, test configs
permutationsYesNon! / (n-r)!Anagrams, routes, rankings
combinationsNoNon! / (r! (n-r)!)Poker hands, teams, lotteries
combinations_with_replacementNoYes(n+r-1)! / (r! (n-1)!)Coins, dice, multisets

Conclusion

These four itertools functions cover all standard combinatoric patterns.

  • Use product for exhaustive cross-iteration across multiple iterables
  • Use permutations when the arrangement order matters
  • Use combinations for unordered selections without repetition
  • use combinations_with_replacement when the same element can appear multiple times.

All four return lazy iterators that generate results on demand, making them memory-efficient for even the largest combinatoric spaces.