Módulo itertools do Python: O Guia Prático para Iteração Elegante

Aprenda a usar o módulo itertools do Python para encadear, fatiar, agrupar e acumular iteradores sem criar listas intermediárias. Guia prático em português brasileiro com chain, islice, groupby, accumulate, product, combinations e receitas de processamento de dados.

11 min de leitura Equipe Python Brasil

O itertools é o módulo da biblioteca padrão do Python dedicado a iteração eficiente. Ele oferece funções pequenas, escritas em C, que criam e combinam iteradores — aqueles objetos que produzem um valor de cada vez sob demanda, sem montar a coleção inteira na memória. Se você já escreveu um loop só para concatenar duas listas grandes, fatiar um gerador infinito ou agrupar vendas por filial, provavelmente estava reinventando o que o itertools já entrega pronto, testado e rápido.

Este guia cobre as ferramentas mais úteis do módulo — chain, islice, groupby, accumulate, starmap, compress, os iteradores infinitos e os de combinatória — com exemplos em contexto brasileiro. Ele complementa conteúdos como Geradores e Iteradores, o módulo collections, Compreensão de Listas e Estruturas de Dados em Python. A ideia não é decorar a API inteira, mas saber qual função pega cada problema.

Por que usar itertools: o ganho da avaliação preguiçosa

A vantagem central do itertools é a avaliação preguiçosa (lazy evaluation). Uma list comprehension como [x**2 for x in range(1_000_000)] aloca um milhão de inteiros na memória antes de você usar o primeiro. Um equivalente com itertools produz cada valor só quando alguém o consome:

from itertools import count, islice

# count() é um iterador infinito — nunca termina, nunca estoura a memória
# islice() fatia os primeiros 5 valores, como range(), mas sobre qualquer iterador
for i in islice(count(1), 5):
    print(i)
# 1 2 3 4 5

Isso permite três coisas que seriam caras ou impossíveis com listas: processar arquivos e fluxos maiores que a RAM, parar no meio com uma condição e encadear transformações sem custo de cópia. Para um pipeline de dados, isso é a diferença entre um script que escala e um que estoura a memória num milhão de linhas.

chain: concatenar iteradores sem custo extra

chain recebe vários iteráveis e devolve um único iterador que percorre todos em sequência, sem criar uma lista intermediária. É a forma correta de iterar sobre várias fontes:

from itertools import chain

vendas_sp = ["SP-001", "SP-002"]
vendas_rj = ["RJ-001"]
vendas_mg = ["MG-001", "MG-002", "MG-003"]

# Errado (copia tudo para uma lista nova):
for v in vendas_sp + vendas_rj + vendas_mg:
    ...

# Certo (percorre cada fonte na ordem, sem alocar nada extra):
for v in chain(vendas_sp, vendas_rj, vendas_mg):
    print(v)
# SP-001 SP-002 RJ-001 MG-001 MG-002 MG-003

A variante chain.from_iterable faz o mesmo quando as fontes já estão numa estrutura — por exemplo, uma lista de listas ou um gerador que devolve geradores:

arquivos_por_filial = [
    ["sp_jan.csv", "sp_fev.csv"],
    ["rj_jan.csv"],
    ["mg_jan.csv", "mg_fev.csv", "mg_mar.csv"],
]

# flatten de uma só vez, sem [[x] for sublist in ... for x in sublist]
for nome in chain.from_iterable(arquivos_por_filial):
    print(nome)

Combine chain.from_iterable com pathlib e você tem um varredor de pastas que processa todos os arquivos de várias filiais em um único for.

islice: fatiar um iterador como uma lista

islice(iterable, stop) ou islice(iterable, start, stop, step) funciona como o fatiamento de lista lista[start:stop:step], mas sobre qualquer iterador — inclusive infinitos ou arquivos grandes:

from itertools import islice

with open("vendas.csv", encoding="utf-8") as f:
    # Pega só as 10 primeiras linhas do arquivo, sem carregar o arquivo inteiro
    for linha in islice(f, 10):
        print(linha.strip())

O islice é a ferramenta certa quando você quer os primeiros N resultados de um gerador caro (uma consulta paginada, uma busca, um modelo de ML) e não pode materializar tudo. Ele não aceita índices negativos como o fatiamento de lista — iteradores só andam para frente.

groupby: agrupar elementos consecutivos

groupby(iterable, key) agrupa elementos consecutivos que compartilham a mesma chave. O detalhe que derruba todo iniciante: ele agrupa apenas consecutivos, não todos os iguais. Por isso a receita padrão é ordenar antes:

from itertools import groupby

vendas = [
    ("SP", 1200), ("RJ", 800), ("SP", 300), ("MG", 500),
    ("RJ", 200), ("SP", 700), ("MG", 900),
]

# 1) ordenar pela chave de agrupamento (obrigatório para juntar todos do mesmo estado)
vendas_ordenadas = sorted(vendas, key=lambda v: v[0])

# 2) agrupar
for estado, grupo in groupby(vendas_ordenadas, key=lambda v: v[0]):
    total = sum(valor for _, valor in grupo)
    print(f"{estado}: R$ {total}")
# MG: R$ 1400
# RJ: R$ 1000
# SP: R$ 2200

O groupby devolve, para cada chave, um iterador temporário que precisa ser consumido antes de avançar para o próximo grupo. Se você guardar o grupo numa lista e continuar, ele estará vazio. Para persistir, materialize: {k: list(g) for k, g in groupby(...)}.

Para contagens de frequência sem ordenação, use Counter do módulo collections — ele é mais direto quando você só quer saber “quantos de cada”.

accumulate: somatório e redução com histórico

accumulate(iterable, func) devolve o total acumulado em cada passo — como um functools.reduce que mantém o histórico de todas as etapas:

from itertools import accumulate
import operator

faturamento = [500, 800, 300, 1000]

# Soma acumulada (comportamento padrão)
list(accumulate(faturamento))
# [500, 1300, 1600, 2600]

# Acumulado com outra operação: o maior valor visto até cada passo
list(accumulate(faturamento, max))
# [500, 800, 800, 1000]

# Produto acumulado
list(accumulate(faturamento, operator.mul))
# [500, 400000, 120000000, 120000000000000]

É a base de indicadores de “running total” em dashboards com Streamlit: faturamento acumulado no ano, saldo de conta corrente, meta acumulada de vendas.

takewhile e dropwhile: parar ou pular por condição

takewhile(condição, iterável) devolve elementos enquanto a condição for verdadeira e para no primeiro falso. dropwhile faz o oposto: descarta enquanto a condição é verdadeira e devolve o restante inteiro:

from itertools import takewhile, dropwhile

logs = ["OK", "OK", "OK", "ERRO", "OK", "ERRO"]

# Pegar até o primeiro ERRO
list(takewhile(lambda x: x == "OK", logs))
# ['OK', 'OK', 'OK']

# Descartar até o primeiro ERRO (depois entrega tudo, mesmo os OK seguintes)
list(dropwhile(lambda x: x == "OK", logs))
# ['ERRO', 'OK', 'ERRO']

São ideais para processar arquivos com cabeçalho/rodapé, parar num marcador ou extrair só a faixa válida de uma série temporal.

starmap e compress: mapear tuplas e filtrar por máscara

starmap(func, iterável_de_tuplas) aplica uma função já desempacotando os argumentos de cada tupla — equivalente a func(*args) em loop:

from itertools import starmap

transacoes = [("compra", 100), ("venda", 200), ("compra", 50)]

def classificar(tipo, valor):
    return f"{tipo.upper()} de R${valor}"

list(starmap(classificar, transacoes))
# ['COMPRA de R$100', 'VENDA de R$200', 'COMPRA de R$50']

compress(dados, seletores) filtra dados por uma sequência paralela de booleanos (uma máscara):

from itertools import compress

emails = ["[email protected]", "[email protected]", "[email protected]"]
validos = [True, False, True]

list(compress(emails, validos))
# ['[email protected]', '[email protected]']

É a forma mais limpa de aplicar o resultado de uma validação vetorizada sem cair num if dentro de comprehension.

Iteradores infinitos: count, cycle e repeat

Três geradores que nunca param sozinhos:

from itertools import count, cycle, repeat

# count(start, step): 0, 1, 2, 3, ... para sempre
for i in count(10, 2):
    if i > 16:
        break
    # 10, 12, 14, 16

# cycle(iter): repete a sequência indefinidamente
turnos = cycle(["manhã", "tarde", "noite"])
for i, t in zip(range(7), turnos):
    print(f"dia {i}: {t}")

# repeat(obj, n): devolve o mesmo objeto n vezes (ou para sempre se n for omitido)
list(repeat("placeholder", 3))
# ['placeholder', 'placeholder', 'placeholder']

Sempre combine iteradores infinitos com islice, takewhile ou zip para garantir o término — um for puro sobre count() roda para sempre.

Combinatória: product, combinations e permutations

A família de combinatória resolve problemas de “todas as possibilidades” sem recursão manual:

from itertools import product, combinations, permutations

# product: produto cartesiano (todas as combinações entre listas)
cores = ["verde", "azul"]
tamanhos = ["P", "M"]
list(product(cores, tamanhos))
# [('verde','P'), ('verde','M'), ('azul','P'), ('azul','M')]

# combinations: subconjuntos de tamanho r, sem ordem, sem repetição
vendedores = ["Ana", "Beto", "Caio", "Duda"]
list(combinations(vendedores, 2))   # duplas para um projeto
# [('Ana','Beto'), ('Ana','Caio'), ('Ana','Duda'), ('Beto','Caio'), ('Beto','Duda'), ('Caio','Duda')]

# permutations: sequências em que a ordem importa
list(permutations("AB", 2))
# [('A','B'), ('B','A')]

Atenção ao tamanho: permutations e product explodem combinatorialmente. permutations(range(10), 10) gera mais de 3,6 milhões de resultados — confirme que você realmente precisa deles antes de pedir.

Receita brasileira: paginar e agrupar vendas

Um caso de uso que junta várias funções: você tem um arquivo grande de vendas e quer (1) mostrar as 5 primeiras linhas como amostra e (2) totalizar por estado. Com itertools, nenhum dado é duplicado na memória:

from itertools import islice, groupby

def carregar_vendas(caminho):
    """Devolve uma lista de (estado, valor) a partir de um CSV estado,valor."""
    linhas = []
    with open(caminho, encoding="utf-8") as f:
        for linha in f:
            estado, valor = linha.strip().split(",")
            linhas.append((estado, int(valor)))
    return linhas

vendas = carregar_vendas("vendas.csv")

# 1) Amostra: as 5 primeiras vendas (sem carregar arquivo inteiro se for um gerador)
print("Amostra:")
for estado, valor in islice(vendas, 5):
    print(f"  {estado}: R$ {valor}")

# 2) Total por estado (ordena antes de agrupar!)
vendas_ordenadas = sorted(vendas, key=lambda v: v[0])
print("Totais por estado:")
for estado, grupo in groupby(vendas_ordenadas, key=lambda v: v[0]):
    print(f"  {estado}: R$ {sum(v for _, v in grupo)}")

O mesmo molde serve para consolidar notas fiscais por mês, agregar métricas de API por rota ou agrupar linhas de log por nível.

itertools vs loops manuais: quando vale a pena

O itertools não substitui todo loop. A regra prática é usar quando você quer lazy evaluation, encadeamento de transformações ou operações que o módulo já tem nome (groupby, accumulate, combinations). Quando a lógica é simples e a lista é pequena, uma list comprehension costuma ser mais legível para quem lê o código depois:

ObjetivoLoop/comprehensionitertools
Concatenar listasa + b + cchain(a, b, c)
Achatar lista de listas[x for sub in L for x in sub]chain.from_iterable(L)
Primeiros N de um geradorlist(g)[:N] (materializa tudo!)islice(g, N)
Agrupar por chavemanual com dictgroupby (após sorted)
Soma acumuladaloop com total += xaccumulate
Todas as duplasdois for aninhadoscombinations(L, 2)

Prefira itertools quando a origem é um iterador/gerador, o volume é grande ou a operação aparece com frequência e tem nome próprio.

Boas práticas

  • Sempre pare iteradores infinitos. Combine count, cycle e repeat com islice, takewhile ou zip para evitar loops eternos.
  • Ordene antes de groupby. O groupby agrupa só consecutivos; sem ordenação prévia pela chave, você terá grupos duplicados.
  • Consuma o grupo na hora. O iterador de cada grupo de groupby é descartado ao avançar; materialize com list(g) se precisar guardar.
  • Prefira iteradores a listas em pipelines grandes. Encadear islicetakewhilemap mantém a memória constante, mesmo com milhões de itens.
  • Cuidado com combinatória. permutations e product crescem fatorialmente; valide o tamanho da entrada antes de pedir todas as combinações.
  • Importe só o que usa. from itertools import chain, groupby é mais legível que itertools.chain espalhado pelo código.

Conclusão

O itertools é a ferramenta que separa o Python “que funciona” do Python “que escala”. Suas funções pequenas, escritas em C, resolvem os padrões mais comuns de iteração — concatenar, fatiar, agrupar, acumular, combinar — mantendo a memória constante e o código curto. Para qualquer processamento de dados que envolva volumes grandes, fluxos infinitos ou agrupamento por regra, comece perguntando “isso já tem no itertools?”.

Para continuar a sequência de fundamentos da biblioteca padrão, veja como combinar itertools com collections para agregar e contar, com geradores e o protocolo de iteração para entender o que acontece por baixo, e com expressões regulares para filtrar as linhas antes de agrupá-las.

Perguntas frequentes

O itertools vem instalado com o Python ou preciso instalar algo?

O itertools faz parte da biblioteca padrão do Python desde a versão 2.3 e não precisa de instalação com pip — basta fazer from itertools import chain, groupby. Ele é escrito em C, então é mais rápido do que reimplementar a mesma lógica com loops e listas em Python puro.

Qual a diferença entre itertools e uma list comprehension?

A list comprehension cria a lista inteira na memória antes de você usá-la; o itertools devolve um iterador que produz os elementos sob demanda (lazy evaluation). Para volumes grandes, o itertools consome pouca memória e permite parar no meio com islice ou takewhile. Para listas pequenas, a list comprehension costuma ser mais legível.

Por que meu groupby não agrupou todos os itens iguais?

O groupby do Python só agrupa elementos consecutivos iguais, e não todos os iguais do iterador. Se os dados não estiverem ordenados pela chave de agrupamento, você terá vários grupos repetidos. A receita correta é ordenar antes: dados_ordenados = sorted(dados, key=chave) e então itertools.groupby(dados_ordenados, key=chave).

Como acumular uma soma parcial (running total) sem escrever um loop?

Use itertools.accumulate(valores). Ele devolve um iterador com o total acumulado em cada passo: list(accumulate([100, 200, 50])) retorna [100, 300, 350]. Você pode passar uma função diferente, como min, max ou operator.mul, para acumular de outra forma.

Qual a diferença entre product, combinations e permutations?

product(A, B) calcula o produto cartesiano (todos os pares possíveis entre A e B, com repetição entre as listas). combinations(iteravel, r) gera subconjuntos de tamanho r sem repetição e sem ordem (o par (a,b) é igual a (b,a) e aparece uma vez). permutations(iteravel, r) gera sequências de tamanho r onde a ordem importa e os elementos não se repetem dentro de cada grupo.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português