itertools.batched: Process Iterables in Batches

Published on: August 29, 2026
Reading time: 5 minutes
A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.

Processing data in chunks is common in APIs, databases, queues, files, and pipelines. Instead of loading an entire collection or writing index-based loops, itertools.batched() groups any iterable into tuples of a chosen size while consuming input lazily. The final batch may be shorter, or it can be rejected with strict=True on Python versions that support that option.

This guide explains how to batch sequences and generators, handle incomplete groups, send requests in blocks, read files, combine batches with executors, control memory and backpressure, design retries, and avoid common mistakes in incremental processing.

Your first batch

from itertools import batched

numbers = range(10)
for batch in batched(numbers, 3):
    print(batch)

The result contains tuples with up to three values: (0, 1, 2), (3, 4, 5), (6, 7, 8), and (9,). The iterable is not converted into one large list.

Lazy consumption

def generate():
    for number in range(1_000_000):
        yield number

first = next(batched(generate(), 100))

Only the first one hundred values are consumed to build the first tuple. This makes the function suitable for large or potentially infinite streams, provided downstream consumers also advance in a controlled way.

The incomplete final batch

By default, the final group may contain fewer than n values. This behavior is appropriate when every input must be processed even when the total length is not divisible by the batch size.

list(batched([1, 2, 3, 4, 5], 2))
# [(1, 2), (3, 4), (5,)]

Strict mode

for batch in batched(data, 4, strict=True):
    process(batch)

With strict=True, an incomplete final group raises ValueError. This is useful for coordinate pairs, binary frames, matrices, or protocols that require exact group sizes. Check your minimum Python version because strict support was added after the original function.

Invalid sizes

The size must be at least one. Zero and negative values are invalid. Validate configuration obtained from environment variables, command-line options, or remote settings before creating the iterator.

Why slicing is not equivalent

batches = [data[i:i + 100] for i in range(0, len(data), 100)]

Slicing works only for indexable sequences and creates all groups immediately. It cannot consume files, generators, database cursors, or arbitrary iterators. batched() accepts any iterable and yields one tuple at a time.

Conceptual implementation

from itertools import islice

def manual_batched(iterable, n):
    iterator = iter(iterable)
    while batch := tuple(islice(iterator, n)):
        yield batch

The official function follows this idea: create an iterator, take up to n values with islice(), and stop when no values remain. Prefer the standard API because it communicates intent and handles edge cases consistently.

Batching API requests

for batch in batched(ids, 100):
    response = client.fetch_many(list(batch))
    save(response)

External APIs often limit IDs per request. The chosen size must also consider payload bytes, rate limits, timeouts, response size, and retry cost. A limit of one hundred IDs does not guarantee every request has the same network weight.

Database inserts

for batch in batched(records, 500):
    cursor.executemany(sql, batch)
    connection.commit()

Batching reduces per-call overhead, but very large transactions can hold locks longer and make rollbacks expensive. Benchmark against the real database and choose transaction boundaries that match required atomicity.

Reading files in groups of lines

with open("events.log", encoding="utf-8") as file:
    for lines in batched(file, 1000):
        process_lines(lines)

A text file is already an iterator over lines. At each step, only the current tuple needs to remain in memory. Line strings normally include their newline character.

Binary data

Iterating directly over a bytes object produces integers. When you need binary blocks, file.read(size) is often more efficient. batched is best when the source already yields logical units such as records, tokens, or decoded messages.

Transforming each batch

for batch in batched(data, 50):
    normalized = [normalize(item) for item in batch]
    write(normalized)

Each batch is a tuple. Convert it to a list only when an API requires mutability or a JSON array.

Combining with executors

from concurrent.futures import ThreadPoolExecutor
from itertools import batched

with ThreadPoolExecutor(max_workers=4) as executor:
    for results in executor.map(process_batch, batched(data, 100)):
        save(results)

This composition submits batches to workers, but you must still consider prefetching, exception propagation, ordering, and pressure on external services. The concurrent.futures guide covers executor behavior.

Asynchronous iterators

itertools.batched() works with synchronous iterables. It cannot consume an async iterator directly. For asynchronous streams, create a helper using async for:

async def async_batched(source, n):
    batch = []
    async for item in source:
        batch.append(item)
        if len(batch) == n:
            yield tuple(batch)
            batch.clear()
    if batch:
        yield tuple(batch)

Add size validation and strict behavior if your application needs them.

Backpressure

Lazy batching limits source consumption, but it does not automatically provide backpressure for an entire pipeline. If every tuple is immediately placed in an unbounded queue, memory can still grow. Use bounded queues, semaphores, or sequential processing.

Choosing a batch size

The optimal size depends on fixed overhead per call, memory per item, latency targets, external limits, and failure cost. Small batches increase overhead. Large batches increase memory, lock duration, latency, and retry impact. Measure representative workloads rather than copying a universal number.

Retries and idempotency

When retrying a failed batch, operations should be idempotent or use idempotency keys. Otherwise, values processed before a timeout may be duplicated. If a service reports individual failures, record results per item or split failing groups.

Batching is not grouping by key

batched cuts the stream by position. It does not collect records sharing a category. Use itertools.groupby() for adjacent values with the same key. Sort first if all equal keys must be adjacent.

Batching is not a sliding window

batched(values, 2) produces non-overlapping pairs such as (a,b), (c,d). pairwise(values) produces overlapping pairs such as (a,b), (b,c), (c,d). Use the operation that matches the algorithm.

Single-pass iterators

The original iterator is consumed. You cannot assume it can be traversed again. Recreate the source or materialize data only when reuse is necessary. Be cautious with tee(), because uneven consumers can create a large hidden buffer.

Errors inside a batch

Define whether one invalid item rejects the entire group or is routed to a dead-letter stream. Financial and inventory operations may require atomic rollback. Analytics imports may prefer processing valid records and recording failures separately.

Observability

Record batch number, item count, duration, attempts, and error class without logging sensitive content. Throughput and latency metrics make batch-size tuning evidence-based.

Compatibility fallback

For older Python versions, implement the documented recipe using islice or provide a compatibility package. Centralize the fallback so application code uses one interface and can be deleted cleanly after the minimum version increases.

Common mistakes

  • Materializing all batches: this removes the lazy-memory benefit.
  • Ignoring a short final group: use strict when exact size matters.
  • Accepting zero from configuration: validate the size.
  • Confusing batches with windows: batches do not overlap.
  • Feeding an unbounded queue: memory can still grow.
  • Retrying non-idempotent operations: effects may be duplicated.

Complete resilient import example

from itertools import batched
import time

def import_records(records, size=200):
    for index, batch in enumerate(batched(records, size), 1):
        for attempt in range(1, 4):
            try:
                insert_batch(batch)
                record_metric("batch_ok", len(batch))
                break
            except TemporaryError:
                if attempt == 3:
                    send_to_failures(index, batch)
                    raise
                time.sleep(2 ** (attempt - 1))

The example limits memory, records progress, and uses exponential backoff. A production implementation should add idempotency, transactions, jitter, cancellation, and structured diagnostics.

Conclusion

itertools.batched() provides a clear, lazy way to consume iterables in chunks. It works with lists, generators, files, cursors, and other streams while making the final-batch policy explicit.

The official Python itertools.batched documentation defines the API. Choose sizes from measurements, control backpressure, and use strict mode whenever incomplete groups represent invalid input.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Folders and directories for Python contextlib.chdir
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    contextlib.chdir: Restore Directories Automatically

    Learn Python contextlib.chdir to change directories temporarily, restore paths safely, isolate tests, and avoid global-state bugs.

    Ler mais

    Tempo de leitura: 5 minutos
    03/09/2026
    Python code execution and performance monitoring
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sys.monitoring: Low-Overhead Instrumentation

    Learn Python sys.monitoring for low-overhead instrumentation with selective events, callbacks, tooling, and safe observability.

    Ler mais

    Tempo de leitura: 5 minutos
    03/09/2026
    Software developer organizing object data with Python operator.attrgetter
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.attrgetter: Sort Objects by Attributes

    Learn Python operator.attrgetter to sort, group, and transform objects by simple or nested attributes with clearer reusable code.

    Ler mais

    Tempo de leitura: 4 minutos
    02/09/2026
    Asynchronous programming with Python asyncio.Runner
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.Runner: Reuse the Event Loop Safely

    Learn Python asyncio.Runner to reuse an event loop, control context, signals, debug mode, cancellation, and safe asynchronous shutdown.

    Ler mais

    Tempo de leitura: 6 minutos
    02/09/2026
    Binary data compression with Zstandard in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Zstandard Streams and Dictionaries

    Learn Python compression.zstd for Zstandard compression, streaming, dictionaries, safe limits, testing, and production workflows.

    Ler mais

    Tempo de leitura: 6 minutos
    01/09/2026
    Python application packaged as an executable zipapp archive
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipapp: Build Executable Apps

    Learn Python zipapp to package applications as executable pyz archives, include dependencies, and distribute tools safely.

    Ler mais

    Tempo de leitura: 5 minutos
    01/09/2026