Many algorithms need to compare each value with the next one: calculate differences, detect state changes, validate order, measure distance, find gaps, and analyze time series. itertools.pairwise() transforms an iterable into overlapping pairs: (a, b), then (b, c), then (c, d), without materializing the entire input.
This guide explains pairwise with lists, generators, and files, delta calculations, transitions, order validation, timestamps, short iterables, mutable objects, asynchronous alternatives, and the difference between overlapping pairs and non-overlapping batches.
Your first pairs
from itertools import pairwise
values = [10, 15, 12, 20]
for previous, current in pairwise(values):
print(previous, current)
The result is (10, 15), (15, 12), and (12, 20). Every middle value participates twice: as the second component of one pair and the first component of the next.
Short inputs
An empty iterable or a source with one value produces no pairs and no exception.
list(pairwise([])) # []
list(pairwise([42])) # []
list(pairwise([1, 2])) # [(1, 2)]
If the application requires at least two observations, validate that contract separately.
Lazy processing
def counter():
number = 0
while True:
yield number
number += 1
pairs = pairwise(counter())
print(next(pairs))
print(next(pairs))
The iterator keeps only the previous value needed for the next pair. It can therefore process large or infinite streams in constant auxiliary memory.
Calculating differences
temperatures = [20.0, 21.5, 19.0, 23.0]
deltas = [current - previous for previous, current in pairwise(temperatures)]
The output has one fewer value than the input because the first observation has no predecessor.
Rates of change
samples = [(0.0, 10.0), (2.0, 14.0), (5.0, 20.0)]
rates = []
for (t1, v1), (t2, v2) in pairwise(samples):
rates.append((v2 - v1) / (t2 - t1))
Validate duplicate timestamps to avoid division by zero and confirm samples are ordered chronologically.
Detecting state transitions
states = ["new", "new", "paid", "shipped", "shipped"]
transitions = [
(before, after)
for before, after in pairwise(states)
if before != after
]
This pattern is useful in audit logs, finite-state machines, workflows, and user journeys. Combine it with enumerate() when the transition position matters.
Checking sorted order
def is_sorted(values):
return all(a <= b for a, b in pairwise(values))
all() returns true for empty and one-element inputs. Logically, no adjacent pair violates order. Add a length rule when your domain requires multiple values.
Consecutive duplicates
duplicates = [a for a, b in pairwise(values) if a == b]
This finds adjacent repeats only. To find duplicates anywhere in the dataset, use a set, Counter, database constraint, or suitable grouping strategy.
Gaps in sequences
ids = [1, 2, 5, 6, 10]
gaps = [(a, b) for a, b in pairwise(ids) if b - a > 1]
The same approach works with dates, sequence numbers, and sensor samples. Define how repeated and decreasing values should be classified.
Distances along a path
from math import hypot
points = [(0, 0), (3, 4), (6, 4)]
total = sum(
hypot(x2 - x1, y2 - y1)
for (x1, y1), (x2, y2) in pairwise(points)
)
This computes path length, not direct distance from the first point to the last.
Comparing consecutive file lines
with open("events.log", encoding="utf-8") as file:
for previous_line, current_line in pairwise(file):
compare(previous_line, current_line)
The file is consumed incrementally. Avoid collecting all returned pairs if constant memory is the objective.
Adding positions
for index, (previous, current) in enumerate(pairwise(values), start=1):
print(index - 1, index, previous, current)
The enumeration index naturally represents the position of the second item.
Pairwise versus zip and slicing
For sequences, older code often used:
pairs = zip(values, values[1:])
This creates a slice and does not generalize directly to arbitrary iterators. Before pairwise, generic implementations often used tee(). The standard function is clearer and avoids manual setup.
Conceptual implementation
def manual_pairwise(iterable):
iterator = iter(iterable)
try:
previous = next(iterator)
except StopIteration:
return
for current in iterator:
yield previous, current
previous = current
The official implementation follows this single-pass idea and stores only one preceding reference.
Pairwise versus batched
from itertools import batched, pairwise
list(pairwise([1, 2, 3, 4]))
# [(1, 2), (2, 3), (3, 4)]
list(batched([1, 2, 3, 4], 2))
# [(1, 2), (3, 4)]
pairwise is a window of size two with a step of one. batched creates non-overlapping blocks.
Larger sliding windows
pairwise is specialized for two values. For windows of three or more, use a deque-based recipe:
from collections import deque
def sliding_window(iterable, n):
window = deque(maxlen=n)
for item in iterable:
window.append(item)
if len(window) == n:
yield tuple(window)
Irregular time series
Do not assume a fixed sampling interval. Calculate the time delta for every pair. Handle time zones, records arriving out of order, duplicate timestamps, clock corrections, and missing observations.
Missing values
When a sequence contains None, decide whether a pair should be skipped, imputed, or marked invalid.
for a, b in pairwise(values):
if a is None or b is None:
continue
process(b - a)
NaN values
NaN propagates through arithmetic and is not equal to itself. Detect it explicitly before validating monotonic order or calculating meaningful changes.
Mutable and reused objects
pairwise keeps a reference to the previous object. If a generator repeatedly mutates and yields the same object, both sides of a pair may refer to the same latest state. Yield independent snapshots when historical values matter.
Exceptions from the source
If the source iterator raises an exception, pairwise propagates it. The retained previous reference is released when the iterator is discarded.
Asynchronous pairwise
The standard function accepts synchronous iterables. An async source needs a helper:
async def async_pairwise(source):
iterator = aiter(source)
try:
previous = await anext(iterator)
except StopAsyncIteration:
return
async for current in iterator:
yield previous, current
previous = current
Finding the largest change
largest = max(
((abs(b - a), a, b) for a, b in pairwise(values)),
default=None,
)
Use default because inputs shorter than two values produce no candidates.
Transitions versus groups
pairwise helps locate boundaries. When the objective is to produce complete runs of equal keys, itertools.groupby() is usually more direct. Use pairwise when the transition itself is the important event.
Common mistakes
- Expecting non-overlapping pairs: use batched for that.
- Forgetting the n-1 output length: the first item has no predecessor.
- Reusing a consumed iterator: many sources are single-pass.
- Ignoring duplicate timestamps: rates may divide by zero.
- Confusing adjacent and global duplicates: pairwise sees neighbors only.
- Using a generator that reuses one mutable object: historical references become misleading.
Complete event-audit example
from itertools import pairwise
ORDER = {
"created": 0,
"paid": 1,
"packed": 2,
"shipped": 3,
"delivered": 4,
}
def audit(events):
problems = []
for previous, current in pairwise(events):
if current.timestamp < previous.timestamp:
problems.append("timestamp moved backward")
if ORDER[current.state] < ORDER[previous.state]:
problems.append(
f"invalid transition: {previous.state} -> {current.state}"
)
return problems
The function validates chronology and state progression in one pass with constant auxiliary memory.
Conclusion
itertools.pairwise() clearly expresses algorithms based on consecutive neighbors. It is lazy, works with any synchronous iterable, and removes the need for slices or manual tee-based recipes.
The official Python itertools.pairwise documentation defines the function. Use it for deltas, transitions, gaps, and validation while remembering that pairs overlap and short inputs yield no result.







