Skip to main content

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')]
Cleaner Syntax with enumerate

You 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

MethodSpeedMemoryBest For
itertools.combinations()Fast (C implementation)Lazy iteratorProduction code
Nested loop / comprehensionSlower (Python-level)Builds full listInterviews, learning
enumerate with slicingModerateBuilds full listReadable 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 + 1 to avoid duplicate and reversed pairs, and iterate directly over the combinations object without converting to a list when processing large datasets.