itertools.batched: procesa iterables por lotes

Publicado el: 29/08/2026
Tempo de leitura: 6 minutos
A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.

Procesar datos en lotes es habitual en APIs, bases de datos, colas, archivos y pipelines. En lugar de cargar toda la colección o escribir loops con índices, itertools.batched() agrupa cualquier iterable en tuplas del tamaño elegido y consume la entrada de forma perezosa. El último lote puede ser más pequeño o puede rechazarse con strict=True en las versiones que ofrecen esa opción.

Esta guía explica cómo dividir secuencias y generadores, manejar grupos incompletos, enviar peticiones por bloques, leer archivos, combinar lotes con ejecutores, controlar memoria y backpressure, diseñar reintentos y evitar errores comunes.

Primer ejemplo

from itertools import batched

numeros = range(10)
for lote in batched(numeros, 3):
    print(lote)

El resultado contiene tuplas de hasta tres valores: (0, 1, 2), (3, 4, 5), (6, 7, 8) y (9,). El iterable no se convierte en una lista completa.

Consumo perezoso

def generar():
    for numero in range(1_000_000):
        yield numero

primero = next(batched(generar(), 100))

Solo se consumen los primeros cien valores para formar el primer lote. Esto permite trabajar con streams grandes o potencialmente infinitos, siempre que el consumidor también avance de manera controlada.

El último lote incompleto

Por defecto, el grupo final puede contener menos de n elementos. Esta semántica es adecuada cuando todos los datos deben procesarse aunque la cantidad total no sea múltiplo del tamaño.

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

Modo strict

for lote in batched(datos, 4, strict=True):
    procesar(lote)

Con strict=True, un grupo final incompleto genera ValueError. Es útil para coordenadas, frames, matrices o protocolos que exigen tamaño exacto. Comprueba la versión mínima porque el parámetro se incorporó después de la función inicial.

Tamaños inválidos

El tamaño debe ser al menos uno. Cero y valores negativos son inválidos. Valida configuraciones procedentes de variables de entorno, CLI o servicios remotos antes de crear el iterador.

Por qué slicing no es equivalente

lotes = [datos[i:i + 100] for i in range(0, len(datos), 100)]

El slicing funciona solo con secuencias indexables y crea todos los grupos inmediatamente. No sirve para archivos, generadores, cursores o iteradores arbitrarios. batched() acepta cualquier iterable y produce una tupla cada vez.

Implementación conceptual

from itertools import islice

def batched_manual(iterable, n):
    iterator = iter(iterable)
    while lote := tuple(islice(iterator, n)):
        yield lote

La función oficial sigue esta idea: crea un iterador, extrae hasta n valores con islice() y termina cuando no quedan elementos. Prefiere la API estándar porque comunica intención y resuelve casos límite.

Peticiones a una API

for lote in batched(ids, 100):
    respuesta = cliente.buscar_muchos(list(lote))
    guardar(respuesta)

Las APIs suelen limitar IDs por petición. El tamaño también debe considerar bytes del payload, rate limits, timeout, tamaño de respuesta y coste de reintento. Cien IDs no siempre significan cien cargas equivalentes.

Inserciones en base de datos

for lote in batched(registros, 500):
    cursor.executemany(sql, lote)
    conexion.commit()

Los lotes reducen overhead, pero transacciones enormes mantienen locks durante más tiempo y hacen más costoso el rollback. Mide en la base real y elige límites que respeten la atomicidad requerida.

Leer líneas de archivo

with open("eventos.log", encoding="utf-8") as archivo:
    for lineas in batched(archivo, 1000):
        procesar_lineas(lineas)

Un archivo de texto ya es un iterador de líneas. En cada paso solo es necesario conservar el lote actual. Las strings normalmente incluyen el salto de línea.

Datos binarios

Iterar directamente sobre bytes produce enteros. Para bloques binarios, archivo.read(tamaño) suele ser más eficiente. batched es apropiado cuando la fuente ya entrega unidades lógicas como registros, tokens o mensajes decodificados.

Transformar cada lote

for lote in batched(datos, 50):
    normalizados = [normalizar(item) for item in lote]
    escribir(normalizados)

Cada lote es una tupla. Conviértelo en lista únicamente cuando una API exija mutabilidad o un array JSON.

Combinar con ejecutores

from concurrent.futures import ThreadPoolExecutor
from itertools import batched

with ThreadPoolExecutor(max_workers=4) as executor:
    for resultados in executor.map(procesar_lote, batched(datos, 100)):
        guardar(resultados)

Esta composición envía grupos a workers, pero todavía debes considerar prefetch, propagación de excepciones, orden y presión sobre servicios externos. El artículo de concurrent.futures en Python explica los ejecutores.

Iteradores asíncronos

itertools.batched() trabaja con iterables síncronos. No consume directamente un async iterator. Para streams asíncronos, crea un helper:

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

Añade validación de tamaño y comportamiento strict cuando el contrato lo exija.

Backpressure

La evaluación perezosa limita el consumo de la fuente, pero no garantiza backpressure en todo el pipeline. Si cada lote se coloca inmediatamente en una cola sin límite, la memoria puede seguir creciendo. Usa colas limitadas, semáforos o procesamiento secuencial.

Elegir el tamaño

El tamaño ideal depende del coste fijo por llamada, memoria por elemento, objetivos de latencia, límites externos y coste de fallos. Grupos pequeños aumentan overhead; grupos grandes aumentan memoria, locks, latencia y efecto de retries. Mide cargas representativas.

Reintentos e idempotencia

Al repetir un lote, las operaciones deben ser idempotentes o usar claves de idempotencia. De lo contrario, elementos procesados antes de un timeout pueden duplicarse. Si el servicio informa fallos individuales, registra resultados por elemento o divide el grupo problemático.

No agrupa por clave

batched corta el flujo por posición. No reúne registros de la misma categoría. Usa itertools.groupby() para valores adyacentes con una clave común y ordena previamente cuando sea necesario.

No es una ventana deslizante

batched(valores, 2) produce pares no solapados: (a,b), (c,d). pairwise(valores) produce pares solapados: (a,b), (b,c), (c,d). Elige según el algoritmo.

Iteradores de una sola pasada

La fuente original se consume. No esperes recorrerla otra vez. Recrea la fuente o materializa solo cuando el reuso sea necesario. Ten cuidado con tee(), porque consumidores desequilibrados crean un buffer oculto.

Errores dentro de un lote

Define si un elemento inválido rechaza todo el grupo o se envía a una cola de fallos. Operaciones financieras o de inventario pueden requerir rollback atómico. Importaciones analíticas pueden procesar válidos y registrar errores por separado.

Observabilidad

Registra número de lote, cantidad, duración, intentos y tipo de error sin publicar contenido sensible. Métricas de throughput y latencia permiten ajustar el tamaño con datos.

Compatibilidad

Para versiones anteriores, implementa el recipe con islice o utiliza una biblioteca de compatibilidad. Centraliza el fallback para que el resto del proyecto use una única interfaz.

Errores comunes

  • Materializar todos los lotes: elimina el beneficio de memoria.
  • Ignorar el grupo final corto: usa strict cuando el tamaño exacto importa.
  • Aceptar tamaño cero: valida configuración.
  • Confundir lotes con ventanas: batched no solapa elementos.
  • Alimentar una cola ilimitada: la memoria puede crecer.
  • Reintentar operaciones no idempotentes: los efectos pueden duplicarse.

Ejemplo completo: importación resiliente

from itertools import batched
import time

def importar(registros, tamaño=200):
    for indice, lote in enumerate(batched(registros, tamaño), 1):
        for intento in range(1, 4):
            try:
                insertar_lote(lote)
                registrar_metrica("lote_ok", len(lote))
                break
            except ErrorTemporal:
                if intento == 3:
                    enviar_a_fallos(indice, lote)
                    raise
                time.sleep(2 ** (intento - 1))

El ejemplo limita memoria, registra progreso y usa backoff exponencial. En producción, añade idempotencia, transacciones, jitter, cancelación y diagnósticos estructurados.

Conclusión

itertools.batched() ofrece una forma clara y perezosa de consumir iterables en bloques. Funciona con listas, generadores, archivos, cursores y otros streams, haciendo explícita la política del último lote.

La documentación oficial de itertools.batched define la API. Elige tamaños a partir de métricas, controla backpressure y usa strict cuando los grupos incompletos sean inválidos.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    cmp_to_key: adapta comparadores antiguos a sorted

    Aprende cmp_to_key en Python para adaptar comparadores antiguos, ordenar con locale, conservar estabilidad y evitar relaciones incoherentes.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    total_ordering: genera comparaciones consistentes

    Aprende total_ordering en Python para generar comparaciones coherentes, devolver NotImplemented, integrar dataclasses y probar órdenes.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    inspect.signature: inspecciona parámetros de funciones

    Aprende inspect.signature en Python para leer parámetros, vincular argumentos, conservar decorators y generar interfaces dinámicas.

    Ler mais

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

    get_origin y get_args: inspecciona tipos genéricos

    Aprende get_origin y get_args en Python para inspeccionar genéricos, uniones, Annotated, Literal, alias y metadatos de runtime.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Striking image of a red-bellied python showcasing its vibrant scales in dramatic lighting.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    LiteralString en Python: cadenas confiables

    Aprende LiteralString en Python para restringir SQL, templates y comandos a cadenas confiables y reducir riesgos de inyección.

    Ler mais

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

    dataclass_transform en Python: clases generadas

    Aprende dataclass_transform en Python para tipar decorators, clases base y metaclases que generan campos, __init__ y métodos.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026