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

    Python code on a laptop for creating dynamic dataclasses
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    make_dataclass decorator: Transform Dataclasses

    Learn how the make_dataclass decorator parameter validates, registers, and transforms dynamic dataclasses safely in Python.

    Ler mais

    Tempo de leitura: 5 minutos
    23/10/2026
    Code and XML structure for canonicalization with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree canonicalize: Normalize XML in Python

    Learn Python ElementTree canonicalize for stable XML comparisons, hashes, namespaces, comments, and signature preparation.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026
    Python programming code displayed on a screen
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    T-strings and templatelib: Structured Interpolation

    Learn Python t-strings and string.templatelib for structured interpolation, safer renderers, logging, and custom processing.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026
    SQLite database and Python code for selective iterdump export
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    SQLite iterdump: Create Selective Dumps in Python

    Learn sqlite3.Connection.iterdump filters for selective SQLite dumps, dependency checks, validation, and safer restoration.

    Ler mais

    Tempo de leitura: 4 minutos
    21/10/2026
    Code and files representing Python data compression
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Fast Zstandard in Python

    Learn Python compression.zstd for fast file and data compression, streaming, integrity checks, and safe decompression.

    Ler mais

    Tempo de leitura: 7 minutos
    21/10/2026
    Configuration file and code structure for Python ConfigParser
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ConfigParser: Read INI Files Without Sections

    Learn Python ConfigParser unnamed sections with validation, typed values, compatibility, layered settings, and safe INI writing.

    Ler mais

    Tempo de leitura: 5 minutos
    20/10/2026