Módulo collections do Python: Counter, defaultdict, deque, namedtuple e mais

Domine o módulo collections do Python com exemplos práticos em português: Counter, defaultdict, deque, namedtuple, OrderedDict e ChainMap para código mais limpo e eficiente.

11 min de leitura Equipe python.dev.br

Quem programa em Python costuma viver com listas, dicionários, tuplas e conjuntos no dia a dia. Eles resolvem a maioria dos problemas. Mas existe um grupo de estruturas especializadas, escondidas na biblioteca padrão, que evitam dezenas de linhas de código repetitivo e bugs sutis: o módulo collections. Se você já escreveu um loop só para contar frequências, inicializou um dicionário com listas vazias ou implementou uma fila com list.pop(0), provavelmente estava reinventando o que o collections já oferece pronto e testado.

Este guia cobre as principais ferramentas do módulo — Counter, defaultdict, deque, namedtuple, OrderedDict e ChainMap — com exemplos práticos em contexto brasileiro. Ele complementa conteúdos como Estruturas de Dados em Python, Geradores e Iteradores, Dataclasses: Guia Completo e Tipagem Estática com mypy. O objetivo não é listar a API inteira, mas mostrar quando cada ferramenta vale a pena e quando é exagero.

Por que o módulo collections existe

A filosofia do collections é simples: oferecer estruturas de dados para casos comuns que o dict, a list e a tuple não resolvem bem. Antes de existirem, todo programador Python reimplementava as mesmas soluções — contadores manuais, dicionários com valor padrão, filas lentas com pop(0) — cometendo os mesmos erros de performance e os mesmos bugs de borda.

Usar collections traz três vantagens concretas:

  1. Código mais curto e legível: uma linha de Counter substitui um loop de cinco linhas.
  2. Performance previsível: deque.popleft() é O(1), enquanto list.pop(0) é O(n).
  3. Menos bugs: defaultdict(list) elimina o padrão if chave not in d: d[chave] = [] que aparece em todo lugar.

O custo é quase zero: tudo fica na biblioteca padrão, sem dependências externas, e funciona em qualquer Python 3.6 ou superior.

Counter: contagem de frequências

Counter é um dicionário especializado em contar objetos. Recebe qualquer iterável e devolve um dict-like em que cada chave é um item e o valor é quantas vezes ele apareceu.

Exemplo básico: palavras mais frequentes

Imagine que você quer descobrir quais linguagens aparecem mais em uma lista de vagas de emprego:

from collections import Counter

vagas = [
    "python", "django", "python", "sql", "python",
    "django", "aws", "python", "sql", "docker",
    "python", "django", "aws", "kafka", "python",
]

contador = Counter(vagas)

# Acessa direto como um dicionário, sem KeyError:
print(contador["python"])  # 6
print(contador["cobol"])   # 0  (ausência vira zero)

# Mais comuns primeiro:
print(contador.most_common(3))
# [('python', 6), ('django', 3), ('sql', 2)]

Note um detalhe importante: acessar uma chave inexistente em Counter retorna 0, não levanta KeyError. Isso elimina um try/except inteiro.

Operações entre contadores

Counter suporta aritmética de conjuntos, o que é muito útil para comparar períodos:

from collections import Counter

vagas_2025 = Counter(python=40, django=20, sql=15, cobol=2)
vagas_2026 = Counter(python=60, django=30, sql=25, rust=10)

# Quais cresceram entre os anos?
crescimento = vagas_2026 - vagas_2025
print(crescimento)
# Counter({'python': 20, 'django': 10, 'sql': 10, 'rust': 10})

# União: pega o máximo de cada chave
uniao = vagas_2025 | vagas_2026
print(uniao["python"])  # 60

# Interseção: pega o mínimo
intersecao = vagas_2025 & vagas_2026
print(intersecao["python"])  # 40

Quando NÃO usar Counter

Counter é ideal quando o valor é uma contagem não-negativa. Se você precisa de um agregador mais genérico (somar valores decimais, manter listas, acumular strings), use defaultdict, que é mais flexível. Misturar Counter com dados que não são contagens gera código confuso.

defaultdict: dicionários com valor padrão

defaultdict resolve o padrão mais repetitivo do Python: criar chaves novas em um dicionário de listas, conjuntos ou inteiros.

Agrupando itens por chave

Sem defaultdict, agrupar vendas por região fica assim:

vendas = [
    ("Sudeste", 1200), ("Sul", 800), ("Sudeste", 1500),
    ("Nordeste", 600), ("Sul", 950), ("Sudeste", 2000),
    ("Norte", 300), ("Nordeste", 700),
]

# Jeito manual, verboso:
por_regiao = {}
for regiao, valor in vendas:
    if regiao not in por_regiao:
        por_regiao[regiao] = []
    por_regiao[regiao].append(valor)

Com defaultdict, três linhas somem:

from collections import defaultdict

por_regiao = defaultdict(list)
for regiao, valor in vendas:
    por_regiao[regiao].append(valor)

# Resultado é um dict comum — pode converter se preferir:
dict(por_regiao)
# {'Sudeste': [1200, 1500, 2000],
#  'Sul': [800, 950],
#  'Nordeste': [600, 700],
#  'Norte': [300]}

Factories comuns

A “fábrica” (primeiro argumento) define qual valor padrão será criado. As mais usadas:

defaultdict(list)       # lista vazia []
defaultdict(set)        # conjunto vazio set()
defaultdict(int)        # inteiro 0 (bom para contar)
defaultdict(float)      # 0.0
defaultdict(dict)       # dict vazio {}

Contando com defaultdict(int)

Apesar de Counter ser o ideal para contagem pura, defaultdict(int) serve quando você quer acumular valores maiores que 1 por ocorrência:

from collections import defaultdict

faturamento_por_cliente = defaultdict(float)
compras = [
    ("Acme Ltda", 1500.00),
    ("Beta ME", 320.50),
    ("Acme Ltda", 2200.00),
    ("Gamma SA", 980.75),
    ("Beta ME", 110.00),
]
for cliente, valor in compras:
    faturamento_por_cliente[cliente] += valor

print(faturamento_por_cliente["Acme Ltda"])  # 3700.0

Quando NÃO usar defaultdict

defaultdict cria chaves implicitamente sempre que você acessa uma chave nova. Isso é ótimo em loops de agregação, mas perigoso quando você só queria consultar:

from collections import defaultdict

d = defaultdict(list)
"loja_inexistente" in d           # False — correto
x = d["loja_inexistente"]         # cria [] e insere a chave!
"loja_inexistente" in d           # True agora — efeito colateral

Para consultas puras, prefira dict.get(chave, default) ou verifique com in antes de acessar. Para armazenamento de configuração, o dicionário comum costuma ser mais seguro porque nunca cria chaves por engano.

deque: filas e pilhas eficientes

A lista do Python é eficiente para operações no final (append, pop), mas lenta no início: remover o primeiro elemento com pop(0) desloca todos os outros, custando O(n). Para filas, onde você insere de um lado e remove do outro, use deque (pronuncia-se “deck”), que oferece append e popleft em tempo constante.

Fila FIFO de processamento

Imagine um worker que consome eventos de um webhook:

from collections import deque

fila = deque()

# Produtor: eventos chegam no final
for evento in ["pedido_criado", "pagamento", "estoque", "nota_fiscal"]:
    fila.append(evento)

# Consumidor: processa do início
while fila:
    evento = fila.popleft()
    print(f"processando {evento}")

deque.popleft() é O(1); list.pop(0) é O(n). Em filas com milhares de itens, a diferença é brutal.

Tamanho fixo: janela deslizante

deque(maxlen=N) cria uma fila de tamanho limitado. Ao inserir além do limite, o item mais antigo é descartado automaticamente. É perfeito para manter apenas os últimos N eventos:

from collections import deque

# Mantém apenas os 5 últimos pedidos por usuário
ultimos_pedidos = {}

def registrar_pedido(usuario, pedido_id):
    if usuario not in ultimos_pedidos:
        ultimos_pedidos[usuario] = deque(maxlen=5)
    ultimos_pedidos[usuario].append(pedido_id)

registrar_pedido("diego", 101)
registrar_pedido("diego", 102)
registrar_pedido("diego", 103)
registrar_pedido("diego", 104)
registrar_pedido("diego", 105)
registrar_pedido("diego", 106)  # descarta o 101

print(list(ultimos_pedidos["diego"]))
# [102, 103, 104, 105, 106]

Sem maxlen, esse padrão exigiria um if len(fila) > 5: fila.popleft() a cada inserção.

Rotação de elementos

deque.rotate(n) desloca os elementos n posições para a direita (ou esquerda, se n for negativo). É útil para algoritmos de round-robin:

from collections import deque

servidores = deque(["srv-br-1", "srv-br-2", "srv-br-3"])

def proximo_servidor():
    servidor = servidores[0]
    servidores.rotate(-1)  # primeiro vira último
    return servidor

for _ in range(6):
    print(proximo_servidor())
# srv-br-1, srv-br-2, srv-br-3, srv-br-1, srv-br-2, srv-br-3

Quando NÃO usar deque

deque é mais lenta que list para acesso por índice no meio (d[500] precisa percorrer a estrutura). Se você precisa de acesso aleatório frequente, fatiamento ou ordenação, use list. Use deque só quando o padrão de uso for: inserções/remoções nas duas pontas.

namedtuple: tuplas com nomes

Tuplas comuns são acessadas por índice (ponto[0], ponto[1]), o que torna o código ilegível rapidamente. namedtuple cria uma subclasse de tupla em que cada posição tem um nome, mantendo a leveza (sem dicionário interno) e a compatibilidade com desempacotamento.

Definição e uso básico

from collections import namedtuple

Ponto = namedtuple("Ponto", ["x", "y"])
p = Ponto(3, 4)

print(p.x)          # 3 — acesso por nome
print(p[0])         # 3 — acesso por índice (compatível com tupla)
x, y = p            # desempacotamento funciona
print(x, y)         # 3 4

Exemplo prático: retorno de função

Retornar vários valores em uma tupla comum perde o significado dos campos. Com namedtuple, o chamador sabe exatamente o que está recebendo:

from collections import namedtuple

Estatisticas = namedtuple("Estatisticas", ["media", "mediana", "desvio_padrao"])

def calcular_estatisticas(valores):
    n = len(valores)
    media = sum(valores) / n
    ordenados = sorted(valores)
    mediana = ordenados[n // 2] if n % 2 else (ordenados[n // 2 - 1] + ordenados[n // 2]) / 2
    variancia = sum((v - media) ** 2 for v in valores) / n
    return Estatisticas(media, mediana, variancia ** 0.5)

stats = calcular_estatisticas([10, 20, 30, 40, 50])
print(f"Média: {stats.media}")              # 30.0
print(f"Mediana: {stats.mediana}")          # 30
print(f"Desvio padrão: {stats.desvio_padrao:.2f}")  # 14.14

namedtuple vs dataclass

Desde o Python 3.7, dataclasses cobrem muitos usos de namedtuple com mais flexibilidade: valores padrão, métodos, mutabilidade opcional, tipagem nativa. Quando escolher cada um?

  • namedtuple: quando o objeto é imutável, leve, precisa ser desempacotável como tupla e consumir pouca memória. Útil para registros em pipelines de dados, chaves de cache, retorno de parsers.
  • dataclass: quando você quer métodos, mutabilidade, valores padrão complexos ou integração profunda com type hints e mypy.

Como regra prática: comece com dataclass. Só troque por namedtuple se a imutabilidade estrita, o baixo consumo de memória ou a compatibilidade com código que espera tuplas forem essenciais.

OrderedDict: dicionários que lembram a ordem de inserção

Desde o Python 3.7, o dict comum já preserva a ordem de inserção. Então por que OrderedDict ainda existe?

A resposta é que OrderedDict oferece operações adicionais que o dict não tem:

from collections import OrderedDict

od = OrderedDict()
od["primeiro"] = 1
od["segundo"] = 2
od["terceiro"] = 3

# move_to_end: reposiciona uma chave
od.move_to_end("primeiro")  # manda para o final
print(list(od.keys()))  # ['segundo', 'terceiro', 'primeiro']

# popitem: remove do início ou do fim
chave, valor = od.popitem(last=False)  # remove o mais antigo
print(chave)  # segundo

Use OrderedDict quando você implementa caches LRU (Least Recently Used), filas priorizadas ou qualquer estrutura em que precisa mover/reorganizar chaves explicitamente. Para o uso comum de dicionário, o dict nativo basta.

ChainMap: concatenar dicionários sem copiar

ChainMap junta vários mapeamentos e busca em todos na ordem, sem criar um novo dicionário. É eficiente quando você tem configurações em camadas (default → arquivo → variáveis de ambiente).

from collections import ChainMap

config_padrao = {"timeout": 30, "retries": 3, "debug": False}
config_arquivo = {"timeout": 60, "debug": True}
config_env = {"debug": False}  # variável de ambiente sobrepõe tudo

config = ChainMap(config_env, config_arquivo, config_padrao)

print(config["timeout"])  # 60 (vem do arquivo)
print(config["retries"])  # 3 (vem do padrão)
print(config["debug"])    # False (env vence)

A vantagem sobre {**padrao, **arquivo, **env} é que ChainMap não copia os dados: ele apenas mantém referências aos mapeamentos originais. Para configurações grandes ou que mudam em runtime, isso evita custo desnecessário.

Pegadinha: escrita afeta só o primeiro mapa

config["novo"] = 1 escreve em config_env, não em todos. ChainMap é uma visão unificada de leitura, não um dicionário fundido para escrita.

Padrões e armadilhas comuns

Apesar de úteis, as ferramentas do collections têm pegadinhas que aparecem em produção. Vamos cobrir as mais frequentes.

1. defaultdict criando chaves por engano

Já vimos que acessar uma chave inexistente em defaultdict cria ela. Em código que só deveria consultar, isso infla o dicionário silenciosamente:

from collections import defaultdict

estoque = defaultdict(int)
# Bug: "verifica" criando a chave
if estoque["produto_inexistente"] == 0:
    print("sem estoque")
# Agora estoque == {"produto_inexistente": 0}

# Correto:
if "produto_inexistente" not in estoque:
    print("sem estoque")

2. Counter com valores negativos

Counter permite subtração que gera valores negativos ou zero, mas most_common ignora-os só parcialmente:

from collections import Counter

a = Counter(x=5, y=3)
b = Counter(x=8, y=1)
diff = a - b  # {'y': 2}  — x sumiu porque virou <= 0
print(diff)   # Counter({'y': 2})

Se você precisa da diferença matemática (incluindo negativos), use aritmética manual ou dict, não Counter.

3. deque não suporta fatiamento

from collections import deque
d = deque(range(10))
d[2:5]  # TypeError: sequence index must be integer, not 'slice'

Para operações que precisam de fatiamento, converta para list ou use itertools.islice.

4. namedtuple vs sobrescrita acidental

namedtuple é imutável, então atribuição falha com AttributeError. Mas nomes de campo que colidem com métodos (count, index) quebram silenciosamente — o Python avisa, mas não impede. Sempre use nomes que não conflitem com métodos de tupla.

Quando cada ferramenta brilha

Resumo prático para escolher rápido:

Precisa de…Use
Contar frequência de itensCounter
Agrupar valores por chave em listas/conjuntosdefaultdict(list) / defaultdict(set)
Somar valores por chavedefaultdict(int) / defaultdict(float)
Fila FIFO ou pilha dupla eficientedeque
Janela deslizante (últimos N itens)deque(maxlen=N)
Registro imutável com campos nomeadosnamedtuple
Cache LRU ou reordenação de chavesOrderedDict
Configurações em camadas sem cópiaChainMap
Estrutura mutável com métodos e tiposdataclass

Conclusão

O módulo collections é uma das partes mais subutilizadas da biblioteca padrão do Python. Ele resolve problemas que aparecem em todo código de produção: contar, agrupar, enfileirar e representar registros. Conhecer essas ferramentas transforma loops de cinco linhas em uma expressão clara, elimina bugs de inicialização e substitui reimplementações lentas por código testado e otimizado.

Para aprofundar, vale revisitar Estruturas de Dados em Python para o embasamento teórico, Geradores e Iteradores para entender como deque e Counter se relacionam com iteráveis, e Boas Práticas em Python 2026 para onde encaixar essas estruturas em código idiomático moderno. O próximo passo natural depois de dominar collections é estudar o módulo itertools, que traz ferramentas equivalentes para iteração preguiçosa e combinação de iteráveis.

E

Equipe python.dev.br

Contribuidor do Python Brasil — Aprenda Python em Português