Skip to main content

How to Iterate a List in Pairs in Python

Processing data in pairs is one of the most common patterns you will encounter in Python programming. Whether you need to combine two lists element by element, compare neighboring values, or split a flat list into fixed-size chunks, Python offers elegant and efficient built-in tools for each scenario.

This guide covers every major approach to iterating a list in pairs, including parallel iteration with zip(), sliding windows with pairwise(), non-overlapping chunking, and handling edge cases like incomplete pairs or unequal list lengths.

Parallel Iteration with zip()

When you have two separate lists and want to loop through them together, element by element, zip() is the standard tool for the job. It takes two or more iterables and yields tuples of corresponding elements:

names = ["Alice", "Bob", "Charlie"]
ages = [24, 30, 18]

for name, age in zip(names, ages):
print(f"{name} is {age}")

Output:

Alice is 24
Bob is 30
Charlie is 18

What happens when lists have different lengths

By default, zip() stops as soon as the shortest iterable is exhausted. This can silently drop data if you are not careful:

names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [24, 30]

for name, age in zip(names, ages):
print(f"{name} is {age}")

Output:

Alice is 24
Bob is 30

Notice that "Charlie" and "Diana" are completely ignored without any warning.

caution

If losing data silently is unacceptable, use zip() with strict=True (Python 3.10+) to raise a ValueError when the iterables have different lengths, or use itertools.zip_longest to pad the shorter list.

Preserving all elements with zip_longest

itertools.zip_longest continues until the longest iterable is exhausted, filling in missing values with a configurable placeholder:

from itertools import zip_longest

names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [24, 30]

for name, age in zip_longest(names, ages, fillvalue="Unknown"):
print(f"{name} is {age}")

Output:

Alice is 24
Bob is 30
Charlie is Unknown
Diana is Unknown

Sliding Window with pairwise()

Sometimes you need to compare or process consecutive elements within a single list. This is known as a sliding window of size two. Starting with Python 3.10, the itertools.pairwise() function handles this directly:

from itertools import pairwise

data = [10, 20, 30, 40]

for current, next_val in pairwise(data):
print(f"{current} -> {next_val}, diff: {next_val - current}")

Output:

10 -> 20, diff: 10
20 -> 30, diff: 10
30 -> 40, diff: 10

pairwise() generates overlapping pairs: (10, 20), (20, 30), (30, 40). Each element appears in two pairs (except the first and last elements).

Sliding window for Python versions before 3.10

If you are working with Python 3.9 or earlier, you can replicate the same behavior using zip() with slicing:

data = [10, 20, 30, 40]

for current, next_val in zip(data, data[1:]):
print(f"{current} -> {next_val}")

Output:

10 -> 20
20 -> 30
30 -> 40
tip

The zip(data, data[1:]) pattern creates a copy of the sliced portion of the list. For very large lists where memory matters, consider building an iterator-based approach instead:

from itertools import islice

data = [10, 20, 30, 40]
it1 = iter(data)
it2 = iter(data)
next(it2) # Advance the second iterator by one position

for current, next_val in zip(it1, it2):
print(f"{current} -> {next_val}")

Output:

10 -> 20
20 -> 30
30 -> 40

Non-Overlapping Chunks with the Iterator Trick

To split a list into consecutive, non-overlapping pairs, you can use the iterator trick. By passing the same iterator to zip() twice, each call to next() advances the same underlying iterator:

data = [1, 2, 3, 4, 5, 6]

it = iter(data)
for x, y in zip(it, it):
print(f"Pair: ({x}, {y})")

Output:

Pair: (1, 2)
Pair: (3, 4)
Pair: (5, 6)

This works because zip() pulls the first value from it, then the second value from the same it, effectively consuming two elements per iteration.

Generalizing to any chunk size

You can extend this pattern to split data into chunks of any size n:

def chunked(iterable, n):
"""Split an iterable into non-overlapping chunks of size n."""
it = iter(iterable)
return zip(*[it] * n)

data = [1, 2, 3, 4, 5, 6, 7, 8, 9]

for chunk in chunked(data, 3):
print(chunk)

Output:

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)

Index-based alternative with range

If you prefer explicit index control, you can use range() with a step value:

data = [1, 2, 3, 4, 5, 6]

for i in range(0, len(data), 2):
pair = data[i:i + 2]
print(pair)

Output:

[1, 2]
[3, 4]
[5, 6]

This approach has the added benefit of naturally handling incomplete pairs at the end of the list, since slicing does not raise an error when the slice extends past the end.

Handling Incomplete Pairs

When the total number of elements is not evenly divisible by the chunk size, the basic zip(it, it) approach silently drops the leftover element:

data = [1, 2, 3, 4, 5]

it = iter(data)
for x, y in zip(it, it):
print(f"({x}, {y})")

Output:

(1, 2)
(3, 4)

The value 5 is lost entirely. To preserve it, use zip_longest with a fill value:

from itertools import zip_longest

data = [1, 2, 3, 4, 5]

it = iter(data)
for x, y in zip_longest(it, it, fillvalue=None):
print(f"({x}, {y})")

Output:

(1, 2)
(3, 4)
(5, None)
warning

Always be deliberate about how you handle leftover elements. Silently dropping data is a frequent source of subtle bugs, especially when processing records, log entries, or any data where every item matters.

Practical Examples

Calculating differences between consecutive values

A classic use case for pairwise() is computing the change between sequential measurements:

from itertools import pairwise

prices = [100, 105, 102, 110, 108]

changes = [b - a for a, b in pairwise(prices)]
print(changes)

Output:

[5, -3, 8, -2]

Building a dictionary from a flat key-value list

When you receive data as a flat list where keys and values alternate, the iterator trick converts it into a dictionary in one line:

flat_data = ["name", "Alice", "age", "25", "city", "NYC"]

it = iter(flat_data)
config = dict(zip(it, it))
print(config)

Output:

{'name': 'Alice', 'age': '25', 'city': 'NYC'}

Comparing parallel data sources for mismatches

Use zip() combined with enumerate() to identify discrepancies between expected and actual results:

expected = [1, 2, 3, 4, 5]
actual = [1, 2, 4, 4, 5]

for i, (exp, act) in enumerate(zip(expected, actual)):
if exp != act:
print(f"Mismatch at index {i}: expected {exp}, got {act}")

Output:

Mismatch at index 2: expected 3, got 4

Quick Reference

GoalSolutionBehavior
Combine two lists in parallelzip(a, b)(a[0], b[0]), (a[1], b[1]), ...
Parallel with paddingzip_longest(a, b)Pads the shorter list with a fill value
Sliding window (overlapping pairs)pairwise(data)(d[0], d[1]), (d[1], d[2]), ...
Non-overlapping chunkszip(it, it)(d[0], d[1]), (d[2], d[3]), ...
Non-overlapping with leftover handlingzip_longest(it, it)Pads incomplete final chunk
Index-based chunkingrange(0, len(data), 2)Handles incomplete pairs via slicing

Summary

Python provides multiple clean and efficient ways to iterate a list in pairs, each suited to a different scenario:

  • Use zip() to iterate two or more lists in parallel, and zip_longest() when you need to preserve elements from the longer list.
  • Use pairwise() (Python 3.10+) or zip(data, data[1:]) for a sliding window over consecutive elements.
  • Use the iterator trick (zip(it, it)) for non-overlapping pair chunking, and combine it with zip_longest when you need to handle incomplete final pairs.
  • Use range() with a step when you need explicit index-based control.

Each pattern eliminates the need for manual index management and produces clean, readable code that follows Pythonic conventions.