itertools es un módulo de la biblioteca estándar que expone bloques de iteración combinables. Su interés no está en sustituir un bucle for por una función de nombre críptico, sino en manipular flujos de datos sin cargarlos nunca por completo en memoria. Cada función devuelve un iterador perezoso: nada se calcula hasta que se consume el resultado. Eso es lo que permite encadenar transformaciones sobre millones de elementos con una huella de memoria constante.

Estas son las funciones que uso realmente, agrupadas por uso, con las trampas que hacen perder tiempo.

Iteradores infinitos: count, cycle, repeat

Estas tres funciones producen flujos sin fin. Solo son útiles asociadas a una condición de parada, de lo contrario el bucle nunca termina.

count(start, step) genera una sucesión aritmética infinita. Práctico para numerar sin gestionar un contador manual.

from itertools import count, islice

for i in count(10, 2):
    if i > 20:
        break
    print(i)  # 10, 12, 14, 16, 18, 20

cycle(iterable) repite un iterable indefinidamente. Típico para alternar entre recursos (round-robin sobre servidores, colores, workers).

from itertools import cycle

colores = cycle(['rojo', 'verde', 'azul'])
for _, color in zip(range(5), colores):
    print(color)  # rojo, verde, azul, rojo, verde

repeat(elem, times) repite un valor. Sin times, es infinito. Su utilidad principal es proporcionar un argumento constante a map o starmap.

from itertools import repeat

list(repeat(7, 3))  # [7, 7, 7]
list(map(pow, range(5), repeat(2)))  # [0, 1, 4, 9, 16] — cada base al cuadrado

La trampa. count(), cycle() y repeat() sin times no se detienen nunca. Hay que acotarlos siempre con islice, takewhile, un zip sobre una secuencia finita, o un break. Un list(count()) bloquea el proceso.

Recortar y filtrar un flujo

islice(iterable, start, stop, step) aplica un recorte tipo slice sobre cualquier iterador, incluso infinito, sin materializarlo en una lista. Es la herramienta de referencia para acotar un flujo.

from itertools import islice, count

list(islice(count(), 5))        # [0, 1, 2, 3, 4]
list(islice(count(), 2, 8, 2))  # [2, 4, 6]

takewhile(pred, iterable) devuelve los elementos mientras el predicado se cumple, y luego se detiene en el primer fallo. dropwhile hace lo contrario: salta los elementos mientras el predicado se cumple, y luego devuelve todo el resto sin reevaluar.

from itertools import takewhile, dropwhile

data = [2, 3, 8, 1, 9, 4]
list(takewhile(lambda x: x < 5, data))  # [2, 3]       — se detiene en el 8
list(dropwhile(lambda x: x < 5, data))  # [8, 1, 9, 4] — salta 2 y 3, conserva el resto

filterfalse(pred, iterable) es el complemento de filter: conserva los elementos para los que el predicado es falso. Más legible que filter(lambda x: not pred(x), ...).

from itertools import filterfalse

list(filterfalse(lambda x: x % 2, range(10)))  # [0, 2, 4, 6, 8] — los pares

compress(data, selectors) filtra data según un segundo iterable de booleanos. Útil cuando la máscara de selección se calcula en otro sitio, por separado de los datos.

from itertools import compress

nombres = ['Alice', 'Bob', 'Carol', 'Dan']
activos = [True, False, True, False]
list(compress(nombres, activos))  # ['Alice', 'Carol']

Combinar y transformar

chain(*iterables) encadena varios iterables en una sola secuencia, sin crear una lista intermedia. Su variante chain.from_iterable aplana un iterable de iterables, ideal para flujos perezosos.

from itertools import chain

list(chain([1, 2], [3, 4], [5]))               # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]]))    # [1, 2, 3, 4]

accumulate(iterable, func, initial) produce resultados acumulados. Por defecto es una suma acumulada, pero sirve cualquier función binaria (max, operator.mul, etc.). El parámetro initial (Python 3.8+) fija un valor de partida.

from itertools import accumulate
import operator

list(accumulate([1, 2, 3, 4]))               # [1, 3, 6, 10] — suma acumulada
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — producto acumulado
list(accumulate([3, 1, 4, 1, 5], max))       # [3, 3, 4, 4, 5] — máximo corriente

starmap(func, iterable) aplica una función a argumentos ya agrupados en tuplas. Es map cuando los argumentos vienen preempaquetados: starmap(f, [(a, b)]) llama a f(a, b).

from itertools import starmap

puntos = [(3, 4), (6, 8), (5, 12)]
list(starmap(lambda x, y: (x**2 + y**2)**0.5, puntos))  # [5.0, 10.0, 13.0]

pairwise(iterable) (Python 3.10+) devuelve los elementos por pares consecutivos solapados. Perfecto para calcular diferencias o comparar cada elemento con el siguiente.

from itertools import pairwise

list(pairwise([1, 2, 3, 4]))  # [(1, 2), (2, 3), (3, 4)]

temps = [10, 13, 12, 18]
[b - a for a, b in pairwise(temps)]  # [3, -1, 6] — variaciones sucesivas

zip_longest(*iterables, fillvalue) fusiona varios iterables como zip, pero se alinea con el más largo rellenando los huecos con fillvalue en lugar de detenerse en el más corto.

from itertools import zip_longest

list(zip_longest([1, 2, 3], ['a', 'b'], fillvalue='?'))
# [(1, 'a'), (2, 'b'), (3, '?')]

groupby: la trampa de ordenar antes

groupby(iterable, key) agrupa los elementos consecutivos que comparten la misma clave. La palabra importante es consecutivos: groupby solo agrupa rachas adyacentes, no ordena. Sobre una entrada no ordenada por la clave, se obtienen grupos fragmentados.

from itertools import groupby

data = [('FR', 'Paris'), ('US', 'NYC'), ('FR', 'Lyon')]

# Mal: no ordenado por país → 'FR' aparece en dos grupos separados
for pais, grupo in groupby(data, key=lambda x: x[0]):
    print(pais, [v for _, v in grupo])
# FR ['Paris']
# US ['NYC']
# FR ['Lyon']

# Bien: ordenar primero por la misma clave
data.sort(key=lambda x: x[0])
for pais, grupo in groupby(data, key=lambda x: x[0]):
    print(pais, [v for _, v in grupo])
# FR ['Paris', 'Lyon']
# US ['NYC']

Es el error más frecuente con esta función: olvidar el sort con la misma clave que el groupby. Otra sutileza, el objeto grupo es un iterador compartido: si se pasa al grupo siguiente sin haber consumido el anterior, su contenido se pierde. La función key se presta bien a operator.itemgetter, más rápida y legible que una lambda.

tee: duplicar un iterador, con prudencia

tee(iterable, n) devuelve n iteradores independientes a partir de uno solo. No copia los datos: los iteradores comparten un buffer interno que retiene todo lo que el más lento aún no ha consumido.

from itertools import tee

it = iter([1, 2, 3, 4])
a, b = tee(it, 2)
list(a)  # [1, 2, 3, 4]
list(b)  # [1, 2, 3, 4]

Dos trampas reales. Primero, no volver a tocar el iterable de origen tras tee: seguir consumiéndolo desincroniza las copias. Segundo, si uno de los iteradores se adelanta mucho al otro, el buffer interno crece para retener los elementos pendientes. Duplicar un flujo y luego consumir por completo la primera copia antes que la segunda equivale a guardarlo todo en memoria, lo que anula la ventaja de la pereza. tee es eficiente solo si las copias avanzan a un ritmo parecido.

Combinatoria: product, permutations, combinations

Estas funciones generan disposiciones. Siguen siendo perezosas, pero cuidado: el número de resultados explota rápido (factorial o exponencial).

product(*iterables, repeat) calcula el producto cartesiano. Sustituye los bucles for anidados.

from itertools import product

list(product([1, 2], ['a', 'b']))   # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product([0, 1], repeat=3))     # todas las combinaciones binarias sobre 3 bits

permutations(iterable, r) genera todas las disposiciones ordenadas de longitud r (el orden importa). combinations(iterable, r) genera los subconjuntos no ordenados de longitud r (el orden no importa). combinations_with_replacement permite además repetir un mismo elemento.

from itertools import permutations, combinations, combinations_with_replacement

list(permutations('ABC', 2))                  # AB AC BA BC CA CB — 6 disposiciones
list(combinations('ABC', 2))                  # AB AC BC          — 3 combinaciones
list(combinations_with_replacement('ABC', 2)) # AA AB AC BB BC CC — 6 con repetición

La trampa. permutations(range(10)) produce 3 628 800 tuplas. Nunca envolver estas funciones en un list() sin acotar el tamaño de la entrada, so pena de saturar la memoria. Iterar directamente con un break o un islice cuando solo se busca una muestra.

Resumen

FunciónPapelA recordar
count / cycle / repeatflujos infinitosacotar con islice o takewhile
isliceslice perezosofunciona sobre un iterador infinito
takewhile / dropwhilecortar según un predicadose detiene / salta en el primer cambio
filterfalse / compressfiltradocomplemento de filter / máscara externa
chainconcatenar iterablesfrom_iterable para aplanar
accumulateresultados acumuladosfunc e initial personalizables
starmapmap sobre tuplas de argumentosf((a, b))f(a, b)
pairwisepares consecutivosPython 3.10+, ideal para deltas
zip_longestzip sin truncarrellena con fillvalue
groupbyagrupar rachasordenar antes por la clave
teeduplicar un iteradorcostoso en memoria si se desincroniza
product / permutations / combinationscombinatoriala salida explota, acotar la entrada

itertools no aporta nada imposible de escribir a mano. Aporta primitivas en C, probadas, que se componen entre sí y preservan la pereza de principio a fin. Donde una comprensión de lista materializa todo, un pipeline itertools procesa un elemento cada vez. En grandes volúmenes, esa es la diferencia entre un script que cabe en memoria y uno que la satura. Para las estructuras de datos complementarias, ver el módulo collections.