itertools.pairwise: Analyze Consecutive Pairs

Published on: August 29, 2026
Reading time: 4 minutes
Detailed close-up of yellow and white albino python scales, capturing texture and pattern.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    itertools.batched: Process Iterables in Batches

    Learn Python itertools.batched to process iterables in chunks, control memory, use strict mode, and build resilient data pipelines.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    cmp_to_key: Use Legacy Comparators with sorted

    Learn Python cmp_to_key to adapt legacy comparators, sort with locale rules, preserve stability, and avoid inconsistent ordering.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person reads 'Python for Unix and Linux System Administration' indoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    total_ordering: Generate Consistent Comparisons

    Learn Python total_ordering to generate consistent comparisons, return NotImplemented, integrate dataclasses, and test ordering rules.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    inspect.signature: Inspect Function Parameters

    Learn Python inspect.signature to read parameters, bind arguments, preserve decorators, and build dynamic callable interfaces safely.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Vivid close-up of a python resting among autumn leaves, showcasing its intricate patterns.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    get_origin and get_args: Inspect Generic Types

    Learn Python get_origin and get_args to inspect generics, unions, Annotated, Literal, aliases, and runtime type metadata safely.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python LiteralString: Trusted Strings

    Learn Python LiteralString to restrict SQL, templates, and commands to trusted strings and reduce injection risks with static analysis.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026