Combinatorics, the branch of mathematics focused on counting techniques, is fundamental to computing. The Python standard library‘s itertools module packs a powerful set of tools for efficiently generating combinatorial patterns like permutations and combinations.
In this comprehensive guide, we‘ll explore these invaluable iterators for intelligently producing combinations – one of the most common combinatorial constructions.
What Are Combinations?
Before diving into Python implementation, let‘s quickly define combinations.
A combination selects a subset of elements from a set without respecting order. For example, the 2-combinations of a set {1, 2, 3} are:
{1, 2}, {1, 3}, {2, 3}
Notice {1, 2} is considered equal to {2, 1} – the ordering does not matter.
This contrasts permutations which do account for order. The 2-permutations of {1, 2, 3} would be:
{1, 2}, {1, 3}, {2, 1}, {2, 3}, {3, 1}, {3, 2}
So while permutations of n elements taken r at a time contain repetitions, combinations do not. There are some useful relationships between the two however.
For instance, the number of r-combinations from a set of n elements is equal to the number of r-permutations divided by r!. We can see this logic applying to our previous example. There are 3 P 2 = 6 2-permutations of {1, 2, 3}, and by dividing 6 by the factorial of 2, we get the 3 above 2-combinations.
Now let‘s see how to generate combinations efficiently in Python.
Enter Python‘s Combinations Iterator
Python‘s itertools.combinations() creates an iterator returning successive r-length combinations from an iterable collection of unique elements.
This acts like an optimized, memory-efficient generator, allowing us to lazily produce combinations as needed without computing all possible ones at once.
Here‘s how we‘d import combinations and get all 2-combinations from [1, 2, 3, 4]:
from itertools import combinations
data = [1, 2, 3, 4]
result = combinations(data, 2)
for combo in result:
print(combo)
# (1, 2)
# (1, 3)
# (1, 4)
# (2, 3)
# (2, 4)
# (3, 4)
combinations() takes two arguments:
- The iterable to produce combinations from
- The length of combinations we want
It returns an iterator we can loop over, printing the yielded tuple combinations one by one.
Let‘s break down some key details:
- The input iterable must contain unique elements. Combinations does not filter duplicates.
- The elements can be any iterable collection like lists, tuples, sets, etc.
- Output combinations are tuples, immutable like input elements.
- The order does not matter. (1, 2) is equal to (2, 1).
Now let‘s walk through some common use cases for combinations().
Generate All Combinations of A Python Sequence
A common application is generating all combinations across a sequence like a string or list.
We can pass sequences directly to combinations() and specify the desired length:
from itertools import combinations
letters = [‘a‘, ‘b‘, ‘c‘, ‘d‘]
result = combinations(letters, 3)
for combo in result:
print(combo)
# (‘a‘, ‘b‘, ‘c‘)
# (‘a‘, ‘b‘, ‘d‘)
# (‘a‘, ‘c‘, ‘d‘)
# (‘b‘, ‘c‘, ‘d‘)
This prints all 3-letter combinations from the letters list.
We can also combine letters from a string:
from itertools import combinations
import string
alphabet = string.ascii_lowercase
print(list(combinations(alphabet, 4)))
Here we take 4-letter combinations across the entire English alphabet string. list() is used to expose the combinations iterator items into a list so we can print all.
This showcases the power of Python one-liners for easily generating potentially large combinatorial outputs.
Combinations with Replacement
The normal combinations() method selects items without replacement. But we can also compute combinations with replacement using combinations_with_replacement():
from itertools import combinations_with_replacement
data = [1, 2, 3]
result = combinations_with_replacement(data, 2)
for combo in result:
print(combo)
# (1, 1)
# (1, 2)
# (1, 3)
# (2, 2)
# (2, 3)
# (3, 3)
Now combinations can contain repeating elements. This allows for modeling use cases like combinations with letter repetitions.
Let‘s take letter combinations from a short string word:
from itertools import combinations_with_replacement
word = ‘intuition‘
result = combinations_with_replacement(word, 4)
for combo in result:
print(‘‘.join(combo))
# intuit
# intui
# intuit
# intui
# ntui
# ntuin
...
We join tuples into a string to display repeating substring combinations. This makes combinations with replacement useful for testing partial match occurrences.
Filtering Combination Results
Since combinations() returns generators, we can pass them into constructs like list comprehensions to filter or transform results:
from itertools import combinations
nums = [1, 2, 3, 4, 5]
combs = combinations(nums, 3) # all 3-combos
odds = [combo for combo in combs if sum(combo) % 2]
print(odds)
# [(1, 3, 5), (1, 3, 5), (1, 5, 3), (1, 5, 3)]
Here we take even/odd sums within a conditional to filter odd 3-number combinations only.
We can also extend combinations iterators with other tools like map():
from itertools import combinations
from operator import mul
from functools import reduce
data = [2, 3, 5, 7]
combos = combinations(data, 3)
products = map(reduce(mul), combos)
print(list(products)) # [210, 42, 105, 210]
We use reduce and mul to find products across each yielded 3-combo. The combo tuples are multiplied element-wise into product scalars.
Combinations to Find Subsets & Subsequences
Combinations excel at subset generation problems.
Let‘s print all possible substrings from a string:
from itertools import combinations
text = ‘hackerrank‘
for size in range(1, len(text)+1):
for combo in combinations(text, size):
print(‘‘.join(combo))
# h
# a
# c
# k
# e
# r
# r
# a
# n
# k
# ha
# ac
# hc
# ...
We iterate over combo sizes using range() and join to print all substrings.
This extends to generating subsets from any list/sequence – for example generating all subset arrays:
from itertools import combinations
original_list = [1, 2, 3, 4]
for size in range(len(original_list) + 1):
for subset in combinations(original_list, size):
print(subset)
# () - the empty subset
# (1,)
# (2,)
# (3,)
# (4,)
# (1, 2)
# (1, 3)
# ...
# (2, 3, 4)
# (1, 2, 3, 4) - the full set
We can also filter subsets based on conditions (sums, lengths, etc).
Improving Performance with Pools
When dealing with extremely large inputs, we can leverage Pool to parallel process combination generation across multiple worker processes:
from itertools import combinations
from multiprocessing import Pool
numbers = [1, 2, ..., 1000000]
def find_sum(combo):
return sum(combo)
if __name__ == ‘__main__‘:
with Pool() as pool:
results = pool.map(find_sum, combinations(numbers, 5))
print(results[:10])
Here we compute 5-number combo sums by distributing the find_sum work across Pool workers. Using pools for embarassingly parallel combinatorial work can drastically improve performance.
Final Thoughts
The Python combinations() iterator from itertools provides a memory-efficient way to algorithmically generate combinations. This unlocks subset generation, combinatorial analysis, sequence manipulation, and more.
Here are some final key takeaways:
- Leverage
combinationsandcombinations_with_replacementfor generating combinatorial patterns from Python sequences - Iterate over yielded combination tuples lazily without computing all
- Filter or transform combinations using conditional comprehensions
- Improve performance through multiprocessing pools
- Employ combinations for common subset generation problems
The itertools module packs many other useful iterators like permutations, product, groupby among others. Be sure to check out the full suite of iterative tools Python offers.
Happy combining!


