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.
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:
| Objetivo | Loop/comprehension | itertools |
|---|---|---|
| Concatenar listas | a + b + c | chain(a, b, c) |
| Achatar lista de listas | [x for sub in L for x in sub] | chain.from_iterable(L) |
| Primeiros N de um gerador | list(g)[:N] (materializa tudo!) | islice(g, N) |
| Agrupar por chave | manual com dict | groupby (após sorted) |
| Soma acumulada | loop com total += x | accumulate |
| Todas as duplas | dois for aninhados | combinations(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,cycleerepeatcomislice,takewhileouzippara evitar loops eternos. - Ordene antes de
groupby. Ogroupbyagrupa 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 comlist(g)se precisar guardar. - Prefira iteradores a listas em pipelines grandes. Encadear
islice→takewhile→mapmantém a memória constante, mesmo com milhões de itens. - Cuidado com combinatória.
permutationseproductcrescem 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 queitertools.chainespalhado 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.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português