Python
programming
algorithms
pairwise-operations
list-manipulation

Operation on every pair of element in a list

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Applying an operation to every pair of elements in a list comes up in similarity scoring, collision checks, graph building, recommendation systems, and many other algorithmic tasks. The correct implementation depends on what kind of pairs you need: unique unordered pairs, ordered pairs, or pairs that include an element with itself. Once that rule is clear, Python gives you clean ways to express it.

Decide Which Pairs You Actually Need

Before writing loops, answer these questions:

  • Does order matter?
  • Should an element be paired with itself?
  • Do you want each pair once or both directions?

Those choices change the number of pairs dramatically. For a list of size n:

  • unique unordered pairs: n * (n - 1) / 2
  • ordered pairs without self-pairs: n * (n - 1)
  • ordered pairs with self-pairs: n * n

If you do not answer that up front, it is easy to write code that silently duplicates work.

Unique Unordered Pairs With combinations

If the operation is symmetric, meaning f(a, b) is the same as f(b, a), use itertools.combinations.

python
1from itertools import combinations
2
3values = [10, 20, 30, 40]
4
5for left, right in combinations(values, 2):
6    print(left, right, "difference =", abs(left - right))

This produces each pair once and never includes self-pairs. That makes it the usual default for distance calculations, overlap checks, and pairwise comparisons where direction does not matter.

Ordered Pairs With product

If direction matters, use the Cartesian product.

python
1from itertools import product
2
3states = ["draft", "review", "published"]
4
5for source, target in product(states, repeat=2):
6    if source != target:
7        print(source, "->", target)

This includes both draft -> review and review -> draft, which is correct for transitions or dependency edges where the relationship is directional.

Nested Loops Still Have a Place

Do not assume itertools is always the answer. Plain index-based loops are still the clearest tool when you need positions, not just values.

python
1items = ["A", "B", "C", "D"]
2
3for i in range(len(items)):
4    for j in range(i + 1, len(items)):
5        print(i, j, items[i] + items[j])

That pattern is useful when you need to update a matrix, correlate two parallel arrays, or keep track of where each element came from.

Wrap the Operation in a Reusable Function

If the pairing rule shows up more than once, hide it behind a helper function so callers do not have to repeat the loop shape.

python
1from itertools import combinations
2from typing import Callable, Iterable, TypeVar
3
4T = TypeVar("T")
5R = TypeVar("R")
6
7
8def apply_to_unique_pairs(items: Iterable[T], fn: Callable[[T, T], R]) -> list[R]:
9    data = list(items)
10    return [fn(a, b) for a, b in combinations(data, 2)]
11
12
13scores = apply_to_unique_pairs([2, 4, 7, 11], lambda a, b: abs(a - b))
14print(scores)

This keeps the pair semantics in one place, which makes the code easier to test and harder to misuse.

Think About Complexity Early

Pairwise work is usually quadratic. A list of 10 items gives you 45 unique unordered pairs. A list of 10,000 items gives you almost 50 million. That growth catches people off guard because the loop looks harmless on toy data.

If your input can get large, consider:

  • filtering candidates before pairing
  • grouping items into buckets first
  • using vectorized libraries for numeric work
  • streaming results instead of storing them all

A generator can help if you only need to consume pair results incrementally.

python
1from itertools import combinations
2
3
4def pair_sums(values):
5    for left, right in combinations(values, 2):
6        yield left, right, left + right
7
8
9for index, result in enumerate(pair_sums([1, 2, 3, 4, 5])):
10    print(result)
11    if index == 2:
12        break

That avoids building a large intermediate list.

Common Pitfalls

The most common mistake is producing duplicate pairs accidentally. If you write two nested loops from 0 to n, you will usually get both (a, b) and (b, a) whether you wanted them or not.

Another issue is forgetting to exclude self-pairs. In some problems they are harmless; in others they completely distort the result.

Developers also underestimate the cost of quadratic algorithms. What works instantly for a list of 100 elements can become unusable for 100,000.

Finally, choose readability over cleverness. A direct combinations call or a simple nested loop is usually better than a dense comprehension nobody wants to debug.

Summary

  • Decide first whether order matters and whether self-pairs are allowed.
  • Use itertools.combinations for unique unordered pairs.
  • Use itertools.product for ordered pairs.
  • Use index-based loops when positions matter as much as values.
  • Remember that pairwise operations usually grow quadratically with input size.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.