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.
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
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)
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
| Goal | Solution | Behavior |
|---|---|---|
| Combine two lists in parallel | zip(a, b) | (a[0], b[0]), (a[1], b[1]), ... |
| Parallel with padding | zip_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 chunks | zip(it, it) | (d[0], d[1]), (d[2], d[3]), ... |
| Non-overlapping with leftover handling | zip_longest(it, it) | Pads incomplete final chunk |
| Index-based chunking | range(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, andzip_longest()when you need to preserve elements from the longer list. - Use
pairwise()(Python 3.10+) orzip(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 withzip_longestwhen 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.