How to Generate All Pairs from a List in Python
Generating all unique pairs from a list is a common task in data processing, algorithm challenges, and statistical analysis. Given [1, 2, 3], the goal is to produce (1, 2), (1, 3), and (2, 3) without duplicates or reversed pairs.
In this guide, you will learn how to generate all unique pairs using itertools.combinations, nested loops, and list comprehensions. Each approach is explained with examples and output so you can choose the right method for your situation.
Using itertools.combinations
The itertools.combinations function is the standard and most efficient solution. It is implemented in C and returns a memory-efficient iterator:
from itertools import combinations
data = [1, 2, 3, 4]
pairs = list(combinations(data, 2))
print(pairs)
Output:
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
The second argument 2 specifies the size of each combination. Every unique pair appears exactly once, and no pair contains the same element twice.
For large datasets, iterate directly over the result without converting to a list. This keeps memory usage constant regardless of how many pairs exist:
from itertools import combinations
data = range(1000)
count = 0
for pair in combinations(data, 2):
count += 1
# Process each pair one at a time
print(f"Total pairs: {count}")
Output:
Total pairs: 499500
Using Nested Loops
When itertools is not available or not allowed (a common constraint in coding interviews), nested loops produce the same result:
data = ["A", "B", "C", "D"]
pairs = []
for i in range(len(data)):
for j in range(i + 1, len(data)):
pairs.append((data[i], data[j]))
print(pairs)
Output:
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
The key detail is range(i + 1, len(data)). Starting the inner loop at i + 1 ensures two things:
- No self-pairing: an element is never paired with itself.
- No duplicate pairs:
(A, B)is generated but(B, A)is not.
Using a List Comprehension
The same nested loop logic can be written as a list comprehension:
data = ["A", "B", "C", "D"]
pairs = [(data[i], data[j])
for i in range(len(data))
for j in range(i + 1, len(data))]
print(pairs)
Output:
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
enumerateYou can avoid index-based access by combining enumerate with slicing:
data = [1, 2, 3, 4]
pairs = [(x, y) for i, x in enumerate(data) for y in data[i + 1:]]
print(pairs)
Output:
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
This reads more naturally: "for each element x, pair it with every element y that comes after it."
A Common Mistake: Generating Duplicate Pairs
A frequent error is starting the inner loop at 0 instead of i + 1, which produces both (A, B) and (B, A) as well as self-pairs like (A, A):
data = ["A", "B", "C"]
# Wrong: includes duplicates and self-pairs
bad_pairs = [(data[i], data[j])
for i in range(len(data))
for j in range(len(data))
if i != j]
print(bad_pairs)
Output:
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
This produces 6 results instead of the expected 3 unique pairs. The if i != j condition prevents self-pairing but does not prevent reversed duplicates. The correct approach starts the inner loop at i + 1:
data = ["A", "B", "C"]
# Correct: unique pairs only
good_pairs = [(data[i], data[j])
for i in range(len(data))
for j in range(i + 1, len(data))]
print(good_pairs)
Output:
[('A', 'B'), ('A', 'C'), ('B', 'C')]
Understanding the Number of Pairs
The number of unique pairs from n elements follows the binomial coefficient formula:
pairs = n * (n - 1) / 2
def expected_pair_count(n):
return n * (n - 1) // 2
print(f"5 elements: {expected_pair_count(5)} pairs")
print(f"10 elements: {expected_pair_count(10)} pairs")
print(f"100 elements: {expected_pair_count(100)} pairs")
Output:
5 elements: 10 pairs
10 elements: 45 pairs
100 elements: 4950 pairs
You can verify this against itertools.combinations:
from itertools import combinations
data = list(range(10))
actual = len(list(combinations(data, 2)))
expected = 10 * 9 // 2
print(f"Actual: {actual}, Expected: {expected}, Match: {actual == expected}")
Output:
Actual: 45, Expected: 45, Match: True
Working with Pairs in Practice
Once you have generated pairs, common operations include filtering, aggregating, or comparing elements:
from itertools import combinations
numbers = [3, 7, 1, 9, 4]
# Find all pairs that sum to 10
pairs_sum_10 = [(a, b) for a, b in combinations(numbers, 2) if a + b == 10]
print(f"Pairs summing to 10: {pairs_sum_10}")
# Find the pair with the smallest difference
min_pair = min(combinations(sorted(numbers), 2), key=lambda p: p[1] - p[0])
print(f"Closest pair: {min_pair}")
Output:
Pairs summing to 10: [(3, 7), (1, 9)]
Closest pair: (3, 4)
Performance Comparison
| Method | Speed | Memory | Best For |
|---|---|---|---|
itertools.combinations() | Fast (C implementation) | Lazy iterator | Production code |
| Nested loop / comprehension | Slower (Python-level) | Builds full list | Interviews, learning |
enumerate with slicing | Moderate | Builds full list | Readable one-liners |
Conclusion
Use itertools.combinations(data, 2) for production code. It is the idiomatic, most efficient, and most readable approach.
- Fall back to nested loops or list comprehensions when the standard library is not available or when you need to understand the underlying logic for an interview.
- Always start the inner loop at
i + 1to avoid duplicate and reversed pairs, and iterate directly over thecombinationsobject without converting to a list when processing large datasets.