Many data-processing tasks are really questions about transitions: Did a measurement increase? How long was the gap between two events? Did a state change? Is a sequence sorted? These problems need neighboring values, not arbitrary pairs.
Python 3.10 added itertools.pairwise() for exactly this pattern. It produces overlapping adjacent pairs lazily, which makes the intent clearer than manual indexing and lets the same code work with lists, generators, files, and other iterables.
The function is small, but using it well requires understanding its boundary behavior and the fact that iterators are consumed as you iterate.
What pairwise produces
Given an iterable containing:
A B C Dpairwise() yields:
(A, B)
(B, C)
(C, D)In Python:
from itertools import pairwise
values = [10, 13, 12, 20]
for previous, current in pairwise(values):
print(previous, current)The output is:
10 13
13 12
12 20Notice that the middle elements participate twice. 13 is the current value in the first pair and the previous value in the second pair. This overlap is what distinguishes adjacent-pair iteration from ordinary grouping into chunks.
The output has one fewer element
If the input contains n values, pairwise() produces n - 1 pairs when n >= 2.
For zero or one input value, it produces no pairs:
from itertools import pairwise
print(list(pairwise([])))
print(list(pairwise([42])))
print(list(pairwise([1, 2])))[]
[]
[(1, 2)]This boundary behavior is often exactly right. There is no adjacent comparison to make when fewer than two values exist.
It can still matter to application logic, however. If a file is expected to contain at least two samples, silently doing zero comparisons may hide invalid input. Validate that requirement separately rather than expecting pairwise() to raise an error.
Compute changes without indexing
A common use is calculating deltas between successive observations:
from itertools import pairwise
readings = [101.2, 101.8, 100.9, 102.4]
deltas = [current - previous for previous, current in pairwise(readings)]
print(deltas)[0.5999999999999943, -0.8999999999999915, 1.5]The floating-point representation visible here is normal binary floating-point behavior; use an appropriate numeric representation or comparison policy if exact decimal arithmetic matters.
The structural advantage is that the code says what it means: compare each value with its predecessor. There is no index arithmetic such as values[i - 1], and therefore no accidental i = 0 boundary to manage.
Check whether values are ordered
Adjacent comparisons are enough to test monotonic ordering:
from itertools import pairwise
def is_non_decreasing(values):
return all(previous <= current for previous, current in pairwise(values))
print(is_non_decreasing([2, 2, 5, 9])) # True
print(is_non_decreasing([2, 7, 5, 9])) # FalseThis implementation considers an empty iterable and a one-element iterable non-decreasing because there is no adjacent pair that violates the condition. That follows from all() returning True for an empty input.
Whether that is valid for your domain is a separate question. If the business rule requires at least two observations, enforce that requirement explicitly.
Find transitions in event streams
Suppose events contain a timestamp and a state:
from itertools import pairwise
events = [
(0, "starting"),
(3, "running"),
(18, "running"),
(25, "stopped"),
]
for previous, current in pairwise(events):
previous_time, previous_state = previous
current_time, current_state = current
if previous_state != current_state:
print(
f"{previous_state} -> {current_state} "
f"after {current_time - previous_time}s"
)This is useful for logs, telemetry, workflow histories, and protocol traces because the comparison is local. You do not need the entire history to decide whether the current event represents a transition from the preceding event.
pairwise is lazy
pairwise() returns an iterator. It does not first convert the entire input into a list.
That makes it suitable for inputs that arrive incrementally:
from itertools import pairwise
def temperatures():
yield 20.1
yield 20.4
yield 21.0
yield 20.7
for previous, current in pairwise(temperatures()):
print(current - previous)Only enough input is consumed to produce the next pair. Conceptually, the operation needs to retain the preceding item while obtaining the next one, rather than storing the whole iterable.
Laziness is especially useful when the source is large or has no natural end. It also means side effects in the source generator occur during iteration, not when pairwise() is constructed.
Early exit leaves the source partially consumed
Lazy iteration has an important ownership consequence. If you pass an existing iterator to pairwise() and stop early, that iterator has already advanced.
from itertools import pairwise
source = iter([10, 20, 30, 40, 50])
for previous, current in pairwise(source):
print(previous, current)
if current == 30:
break
print(list(source))Do not write code that depends on reusing source from its original position afterward. Iterators generally represent a consumable stream, not a rewindable collection.
If multiple consumers need independent access, decide that at the ownership boundary. Sometimes materializing a bounded input is appropriate. In other cases the source should be reopened or regenerated. Avoid casually duplicating an unbounded stream just to preserve a convenient API.
pairwise is not batching
pairwise() and itertools.batched() solve different problems.
For input A B C D, adjacent pairing gives:
(A, B) (B, C) (C, D)Batching into groups of two gives:
(A, B) (C, D)Use pairwise() when each item must be compared with the item immediately before or after it. Use batching when items should be partitioned into non-overlapping groups for processing.
Confusing these operations can silently omit transitions. With batching, the relationship between B and C disappears entirely.
It is a window of exactly two
pairwise() is best understood as a specialized sliding window with width two. It does not directly produce windows of three or more elements.
If you need to compare triples such as (A, B, C), (B, C, D), use a sliding-window implementation appropriate to your supported Python version rather than nesting pairwise() and creating a harder-to-reason-about structure.
Likewise, if you need every possible pair, pairwise() is the wrong operation. For example, checking every pair of distinct elements is a combinatorial problem and may call for itertools.combinations() instead.
Choosing the operation based on the relationship you need is more important than the superficial fact that each result happens to contain two values.
Preserve domain boundaries
Adjacent values are only meaningful when they belong to the same logical sequence.
Imagine records sorted by customer and time:
customer A, event 1
customer A, event 2
customer B, event 1
customer B, event 2Running one pairwise() across the entire stream creates a pair spanning customer A and customer B. That transition is usually meaningless.
Group or partition the data first, then apply pairwise() inside each domain boundary:
from itertools import groupby, pairwise
from operator import itemgetter
records = [
("A", 1, "queued"),
("A", 2, "sent"),
("B", 1, "queued"),
("B", 3, "sent"),
]
for customer, customer_records in groupby(records, key=itemgetter(0)):
for previous, current in pairwise(customer_records):
print(customer, previous, current)groupby() itself groups consecutive values with the same key, so the input must already have the ordering needed for that grouping. Do not assume it performs a database-style grouping of arbitrary input.
Ordering is part of correctness
pairwise() uses iteration order. It does not sort values and does not know what chronological, numeric, or semantic order your application intended.
For timestamped records, make the ordering guarantee explicit. Data returned by a database should use an appropriate ORDER BY; data read from multiple sources may need merging or sorting; filesystem enumeration should not be assumed to represent chronological order.
Sorting also has a cost: it normally requires collecting the data. If the source already guarantees the correct order, preserving streaming behavior can be much cheaper than sorting again.
The key is to know where the ordering guarantee comes from.
Do not hide validation inside comparisons
Consider timestamp gaps:
from itertools import pairwise
def gaps(events):
for previous, current in pairwise(events):
yield current.timestamp - previous.timestampThis code assumes every event has a usable timestamp and that events arrive in the intended order. If negative gaps are invalid, detect them deliberately:
from itertools import pairwise
def gaps(events):
for previous, current in pairwise(events):
gap = current.timestamp - previous.timestamp
if gap < 0:
raise ValueError("events are not in chronological order")
yield gappairwise() supplies structure; it does not supply domain validation. Keeping those responsibilities separate makes failures easier to understand.
A manual loop can still be appropriate
Before Python 3.10, adjacent iteration was commonly written manually. A manual implementation is also useful when you need unusual state or error handling:
def adjacent(iterable):
iterator = iter(iterable)
try:
previous = next(iterator)
except StopIteration:
return
for current in iterator:
yield previous, current
previous = currentFor ordinary adjacent pairs on Python 3.10 or newer, itertools.pairwise() communicates the pattern directly and avoids maintaining a local helper.
If your package supports Python 3.9 or older, however, importing pairwise will fail. Either keep a compatible helper, use a dependency that provides the operation, or raise the project’s minimum Python version deliberately. Do not accidentally introduce a runtime-version requirement through a seemingly small refactor.
Test boundaries, not just the happy path
Tests for adjacent-pair logic should cover more than a typical list. Useful cases include:
- an empty input;
- a one-element input;
- exactly two elements;
- repeated values;
- a generator rather than a list;
- early termination;
- invalid ordering when ordering is a domain requirement;
- boundaries between logical groups.
For example:
from itertools import pairwise
def test_pairwise_boundaries():
assert list(pairwise([])) == []
assert list(pairwise([1])) == []
assert list(pairwise([1, 2])) == [(1, 2)]
assert list(pairwise([1, 2, 3])) == [(1, 2), (2, 3)]These tests document the most important cardinality rule: adjacent comparison reduces the number of outputs by one.
Keep the mental model small
itertools.pairwise() does one thing: it lazily exposes each value together with the value immediately following it. It does not sort, validate, batch, rewind, or compare values for you.
That narrow contract is why it is useful. Once ordering and domain boundaries are established, adjacent-difference calculations, transition detection, monotonicity checks, interval measurements, and many other tasks become straightforward.
Use pairwise() when the relationship you care about is local and consecutive. Then keep validation, ordering, grouping, and iterator ownership explicit around it. The result is usually simpler than index arithmetic and remains usable for streams that were never sequences in the first place.