Se listas organizam dados por posição, dicionários organizam dados por significado. Em vez de acessar um valor pelo índice 0 ou 3, você o acessa por um nome descritivo — uma chave. Dicionários são a estrutura de dados mais versátil do Python e aparecem em praticamente todo código profissional: configurações, respostas de APIs, registros de banco de dados e muito mais.
Criando Dicionários
# Sintaxe literal com chaves
aluno = {
"nome": "Ricardo",
"idade": 35,
"curso": "Python",
"ativo": True
}
# Construtor dict()
produto = dict(nome="Teclado", preco=299.90, estoque=15)
# Dicionário vazio
config = {}
config = dict()
Acessando Valores
aluno = {"nome": "Ana", "nota": 9.5, "cidade": "Recife"}
# Por chave
print(aluno["nome"]) # Ana
# Com get() — retorna None se a chave não existir (sem erro)
print(aluno.get("nota")) # 9.5
print(aluno.get("email")) # None
print(aluno.get("email", "não informado")) # valor padrão
Prefira .get() quando não tem certeza se a chave existe — acessar uma chave inexistente com [] gera KeyError.
Modificando Dicionários
aluno = {"nome": "Bruno", "nota": 7.0}
# Adicionando ou atualizando
aluno["email"] = "bruno@email.com"
aluno["nota"] = 8.5
# Atualizando múltiplos valores de uma vez
aluno.update({"nota": 9.0, "cidade": "Salvador"})
print(aluno)
# {'nome': 'Bruno', 'nota': 9.0, 'email': 'bruno@email.com', 'cidade': 'Salvador'}
Removendo Entradas
dados = {"a": 1, "b": 2, "c": 3, "d": 4}
# pop() — remove e retorna o valor
valor = dados.pop("b")
print(valor) # 2
# pop() com padrão — evita KeyError
valor = dados.pop("z", None)
# del — remove sem retornar
del dados["c"]
# popitem() — remove e retorna o último par inserido (Python 3.7+)
chave, valor = dados.popitem()
# clear() — esvazia o dicionário
dados.clear()
Iterando sobre Dicionários
estoque = {"maçã": 50, "banana": 30, "laranja": 45}
# Iterando sobre chaves (padrão)
for fruta in estoque:
print(fruta)
# Iterando sobre valores
for quantidade in estoque.values():
print(quantidade)
# Iterando sobre pares chave-valor
for fruta, quantidade in estoque.items():
print(f"{fruta}: {quantidade} unidades")
Uma regra que vale a pena guardar antes de precisar dela: não altere o tamanho do dicionário enquanto o percorre. Diferente da lista, que falha em silêncio e devolve resultado errado, o dicionário levanta erro na hora:
estoque = {"maçã": 50, "banana": 0, "laranja": 45}
for fruta in estoque:
if estoque[fruta] == 0:
del estoque[fruta] # RuntimeError: dictionary changed
# size during iteration
Ser um erro barulhento é uma sorte, não um defeito — o equivalente em lista passa despercebido por meses. A saída é percorrer uma cópia das chaves, for fruta in list(estoque):, ou construir um dicionário novo com uma compreensão, que costuma ser mais claro: estoque = {k: v for k, v in estoque.items() if v > 0}. Alterar apenas o valor de uma chave existente é seguro; o que quebra é inserir ou remover.
O que pode ser chave
Qualquer objeto hasheável serve de chave, o que na prática quer dizer imutável: texto, número, tupla de imutáveis, frozenset. Lista não serve.
{[1, 2]: "x"} # TypeError: cannot use 'list' as a dict key
# (unhashable type: 'list')
{(1, 2): "x"} # a tupla serve, porque é hasheável
É a mesma exigência dos conjuntos, tratada em Tuplas e Sets: imutabilidade e unicidade, e ela traz junto a mesma surpresa: bool é subclasse de int, e True == 1. Para o dicionário, os dois são a mesma chave.
d = {1: "um"}
d[True] = "verdadeiro"
print(d) # {1: 'verdadeiro'}
Note o detalhe que engana: a chave continua sendo 1, não True. O dicionário mantém a chave que já estava e troca apenas o valor. Quando identificadores e indicadores booleanos chegam pela mesma coluna de um arquivo, isso destrói registros sem levantar erro nenhum.
Verificando Existência de Chaves
config = {"debug": True, "host": "localhost", "porta": 8080}
if "debug" in config:
print("Modo debug ativo")
if "timeout" not in config:
config["timeout"] = 30
Métodos Úteis
d = {"x": 10, "y": 20, "z": 30}
print(d.keys()) # dict_keys(['x', 'y', 'z'])
print(d.values()) # dict_values([10, 20, 30])
print(d.items()) # dict_items([('x', 10), ('y', 20), ('z', 30)])
print(len(d)) # 3
# os três devolvem VIEWS, não listas: refletem o dicionário ao vivo
chaves = d.keys()
d["novo"] = 40
print(chaves) # dict_keys(['x', 'y', 'z', 'novo']) — mudou sozinha
chaves[0] # TypeError: 'dict_keys' object is not subscriptable
# Copiando o primeiro nível — veja a ressalva abaixo
copia = d.copy()
# setdefault — insere chave com valor padrão se não existir
d.setdefault("w", 0)
print(d["w"]) # 0
O copy() merece a mesma ressalva que a lista recebeu em Listas: criação, manipulação e métodos: ele é raso. Copia o primeiro nível e preenche o dicionário novo com os mesmos objetos que estavam no antigo. Enquanto os valores forem números ou textos, nada acontece — o problema aparece exatamente na estrutura que a seção de dicionários aninhados vai mostrar adiante:
usuarios = {"ana": {"pontos": 1500}}
copia = usuarios.copy()
copia["ana"]["pontos"] = 0
print(usuarios) # {'ana': {'pontos': 0}} — o original mudou
print(copia["ana"] is usuarios["ana"]) # True — é o mesmo objeto
Trocar copia["ana"] inteiro por outro dicionário não afeta o original, porque aí se mexe no nível de fora. O que atravessa é a alteração dentro de um valor compartilhado. Quando se precisa de independência em qualquer profundidade, o caminho é copy.deepcopy(), que reconstrói a estrutura toda e por isso custa caro em dado grande. Havendo um nível só de aninhamento, uma compreensão resolve melhor: {k: v.copy() for k, v in usuarios.items()}.
Dict Comprehension
Assim como list comprehensions, dicionários têm sua versão compacta:
# Quadrados de 1 a 5
quadrados = {n: n ** 2 for n in range(1, 6)}
print(quadrados) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Filtrando itens
estoque = {"maçã": 50, "banana": 0, "laranja": 45, "uva": 0}
disponiveis = {k: v for k, v in estoque.items() if v > 0}
print(disponiveis) # {'maçã': 50, 'laranja': 45}
# Invertendo chaves e valores
codigos = {"BR": "Brasil", "US": "Estados Unidos", "DE": "Alemanha"}
invertido = {v: k for k, v in codigos.items()}
print(invertido) # {'Brasil': 'BR', ...}
A inversão só é segura quando os valores são únicos, e isso quase nunca está garantido nos dados de verdade. Havendo valor repetido, o dicionário invertido perde entradas em silêncio, porque a segunda chave igual sobrescreve a primeira:
codigos = {"BR": "Brasil", "PT": "Portugal", "AO": "Portugal"}
invertido = {v: k for k, v in codigos.items()}
print(len(codigos)) # 3
print(len(invertido)) # 2 — uma entrada sumiu
print(invertido) # {'Brasil': 'BR', 'Portugal': 'AO'}
Repare que sobrou AO, o último a entrar, e não há aviso nenhum. Se a intenção é agrupar em vez de inverter, o valor da nova chave precisa ser uma lista, montada com setdefault ou com defaultdict — a diferença entre as duas leituras do mesmo dado é a diferença entre um relatório certo e um que perde linhas sem ninguém notar.
Dicionários Aninhados
Dicionários podem conter outros dicionários — estrutura comum em dados de APIs e configurações:
usuarios = {
"ana": {
"email": "ana@email.com",
"nivel": "admin",
"pontos": 1500
},
"bruno": {
"email": "bruno@email.com",
"nivel": "usuario",
"pontos": 320
}
}
# Acessando dados aninhados
print(usuarios["ana"]["email"]) # ana@email.com
print(usuarios["bruno"]["pontos"]) # 320
# Iterando
for usuario, dados in usuarios.items():
print(f"{usuario} ({dados['nivel']}): {dados['pontos']} pontos")
Mesclando Dicionários
A partir do Python 3.9, há uma sintaxe elegante para mesclar dicionários:
padroes = {"tema": "claro", "idioma": "pt-BR", "timeout": 30}
personalizados = {"tema": "escuro", "fonte": "JetBrains Mono"}
# Operador | — cria novo dicionário mesclado
config = padroes | personalizados
print(config)
# {'tema': 'escuro', 'idioma': 'pt-BR', 'timeout': 30, 'fonte': 'JetBrains Mono'}
# Operador |= — atualiza in-place
padroes |= personalizados
Em versões anteriores ao Python 3.9, use {**padroes, **personalizados}.
Exemplo Completo: Frequência de Palavras
def contar_palavras(texto):
"""Conta a frequência de cada palavra em um texto."""
palavras = texto.lower().split()
frequencia = {}
for palavra in palavras:
# Remove pontuação simples
palavra = palavra.strip(".,!?;:")
frequencia[palavra] = frequencia.get(palavra, 0) + 1
return frequencia
def top_palavras(frequencia, n=5):
"""Retorna as n palavras mais frequentes."""
ordenado = sorted(frequencia.items(), key=lambda item: item[1], reverse=True)
return ordenado[:n]
texto = """
Python é uma linguagem incrível. Python é simples e Python é poderoso.
Aprender Python vale muito a pena. A linguagem Python cresce a cada ano.
"""
freq = contar_palavras(texto)
print("Top 5 palavras:")
for palavra, count in top_palavras(freq):
print(f" {palavra:>10}: {count}x")
Saída:
Top 5 palavras:
python: 5x
é: 3x
a: 3x
linguagem: 2x
uma: 1x
Vale olhar o quinto lugar antes de seguir. As palavras é e a empatam em 3, e depois delas vem uma dúzia de palavras com 1 ocorrência cada — uma, incrível, simples, poderoso e as outras. Qual delas aparece no topo dos empatados não é escolha do sorted: a ordenação do Python é estável, isto é, preserva a ordem anterior entre itens de mesma chave, e a ordem anterior aqui é a de inserção no dicionário, que é a ordem em que as palavras surgiram no texto. Por isso sai uma, a primeira palavra de contagem 1 a aparecer. Trocar uma vírgula do texto muda esse quinto lugar sem mudar nada mais, e um relatório que dependa disso vai parecer instável sem motivo. Quando o critério de desempate importa, ele precisa estar escrito: key=lambda item: (-item[1], item[0]) ordena por contagem decrescente e, no empate, em ordem alfabética.
Quando a biblioteca padrão já resolveu
O exemplo de frequência acima é didático, mas em código de produção ele não seria escrito assim. Contar ocorrências é tão comum que o módulo collections traz a estrutura pronta, e as duas funções da seção anterior viram duas linhas:
from collections import Counter
freq = Counter(p.strip(".,!?;:") for p in texto.lower().split())
print(freq.most_common(5))
# [('python', 5), ('é', 3), ('a', 3), ('linguagem', 2), ('uma', 1)]
O resultado é idêntico ao das vinte linhas anteriores. O Counter é um dicionário de verdade — aceita tudo o que este artigo mostrou — com dois acréscimos que importam: devolve 0 em vez de KeyError para chave ausente, e tem o most_common, que já resolve a ordenação.
O parente próximo é o defaultdict, para quando o valor acumulado é uma lista ou um conjunto. Ele dispensa o setdefault repetido a cada volta do laço:
from collections import defaultdict
por_nivel = defaultdict(list)
for nome, dados in usuarios.items():
por_nivel[dados["nivel"]].append(nome)
# {'admin': ['ana'], 'usuario': ['bruno']}
Uma ressalva antes de sair usando: consultar uma chave inexistente num defaultdict cria essa chave. Se o código só quer verificar, e não inserir, use in ou .get() — o acesso com colchetes tem efeito colateral aqui, ao contrário do dicionário comum.
O dicionário é a estrutura mais usada do Python porque troca posição por significado: o dado passa a ser acessado pelo nome que ele tem no domínio, e não pelo lugar onde calhou de cair. O que este artigo acrescenta ao uso básico são as bordas, que é onde o código costuma quebrar. Chave precisa ser hasheável, e como bool é subclasse de int, um True vindo da mesma coluna que um identificador sobrescreve a chave 1 sem erro nenhum. O copy() é raso, e num dicionário aninhado a cópia e o original passam a compartilhar os mesmos valores internos. Inverter chaves e valores perde entradas em silêncio quando há valor repetido.
Duas outras valem guardar pelo contraste. Alterar o tamanho do dicionário durante a iteração levanta RuntimeError na hora, enquanto o equivalente em lista falha calado e devolve resultado errado — aqui o erro barulhento é sorte, não defeito. E keys(), values() e items() não devolvem listas, e sim visões ligadas ao dicionário vivo, que mudam sozinhas e não aceitam índice. Por fim, vale olhar o módulo collections antes de escrever um contador à mão: o Counter e o defaultdict resolvem em duas linhas o que o exemplo completo levou vinte para fazer, e resolvem melhor.
Fontes e leituras recomendadas
- Dicionários — documentação oficial — https://docs.python.org/3/tutorial/datastructures.html#dictionaries
- dict.get(), setdefault(), update() — https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
- PEP 584 — operador de união para dicionários — https://peps.python.org/pep-0584/
- Dict comprehensions (PEP 274) — https://peps.python.org/pep-0274/
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 3 — análise profunda de dicionários e sets como tabelas hash.
- GOODRICH, Michael T. et al. Data Structures and Algorithms in Python. Wiley, 2013. Cap. 10 — mapas, tabelas hash e implementações.
- HUNT, John. Advanced Guide to Python 3 Programming. Springer, 2019. Cap. 4 — uso avançado de coleções em Python.
Exercícios
Exercício 1
Um sistema guarda as preferências padrão num dicionário aninhado e, para cada usuário que entra, faz prefs = PADRAO.copy() antes de aplicar as escolhas dele. Funcionou por meses. Depois que o time acrescentou um subdicionário notificacoes dentro do padrão, as escolhas de um usuário começaram a aparecer para todos. Explique e corrija.
Ver resposta
✓ Resposta: O copy() de dicionário é raso: cria um dicionário novo, mas o preenche com os mesmos objetos que estavam no original. Enquanto os valores foram números, textos e booleanos, nada aconteceu, porque atribuir prefs["tema"] = "escuro" troca a referência dentro da cópia e não toca no padrão. No instante em que um valor passou a ser outro dicionário, prefs["notificacoes"] e PADRAO["notificacoes"] passaram a ser o mesmo objeto — prefs["notificacoes"] is PADRAO["notificacoes"] devolve True — e prefs["notificacoes"]["email"] = False escreveu direto no padrão global, de onde todos os usuários seguintes leram. O detalhe cruel é que o defeito não nasceu do código que quebrou; nasceu meses antes, e só ficou visível quando a estrutura ganhou profundidade. Há três saídas, em ordem de preferência. A primeira é não ter estado global mutável: uma função que devolve os padrões recém-construídos a cada chamada elimina a classe inteira de problema. A segunda, quando há um nível de aninhamento, é a compreensão {k: (v.copy() if isinstance(v, dict) else v) for k, v in PADRAO.items()}, explícita sobre o que está sendo protegido. A terceira é copy.deepcopy(PADRAO), que reconstrói tudo e resolve em qualquer profundidade, mas percorre a estrutura inteira e não deve virar reflexo em dado grande. Vale notar que a mesma armadilha existe em lista, com copy(), [:] e list(), e que o dicionário não é um caso especial: é a mesma regra de que copiar o recipiente não copia o conteúdo.
Exercício 2
Uma importação lê identificadores de uma planilha e monta registros[id] = linha. Uma coluna de indicador booleano foi exportada na mesma posição do identificador em algumas linhas, de modo que chega True onde deveria chegar um número. O arquivo tem 5.000 linhas e o dicionário final tem 4.999, sem nenhum erro no log. O que aconteceu, e por que isinstance(id, int) não protegeria?
Ver resposta
✓ Resposta: Em Python, bool é subclasse de int e True == 1, com hash(True) == hash(1). Como dicionário decide identidade por hash e igualdade, True e 1 são literalmente a mesma chave: a linha que chegou com True sobrescreveu a linha de identificador 1, e o dicionário passou de 5.000 para 4.999 entradas sem uma palavra de aviso. Há um detalhe que confunde na hora de depurar: a chave que fica é a que já estava, e só o valor é trocado. Depois de d = {1: "um"} seguido de d[True] = "verdadeiro", o resultado é {1: 'verdadeiro'} — inspecionar as chaves mostra 1, um inteiro de aparência inocente, e não há vestígio do True que causou o estrago. O mesmo vale para 1.0, que também colapsa. Quanto ao isinstance: ele não protege justamente por causa da herança, já que isinstance(True, int) devolve True — a checagem passa e deixa o booleano entrar. Para barrar, é preciso type(id) is int, que compara o tipo exato, ou rejeitar bool antes com if isinstance(id, bool): raise .... A correção de fundo, porém, é de fronteira e não de checagem: dado que vem de planilha ou CSV chega como texto e precisa ser convertido e validado explicitamente na entrada, com contagem de linhas lidas comparada à de registros gravados. Um importador que não compara essas duas contagens perde linhas em silêncio por muitos outros motivos além deste.
Exercício 3
Para montar um índice reverso de códigos de país, alguém escreve invertido = {v: k for k, v in codigos.items()}. O dicionário original tem 250 entradas; o invertido tem 243. Ninguém percebeu por semanas. Explique o que se perdeu e mostre a forma correta quando o objetivo é agrupar.
Ver resposta
✓ Resposta: A inversão só preserva o número de entradas quando os valores do dicionário original são únicos, e dados de verdade raramente garantem isso. Sempre que dois códigos apontam para o mesmo nome, a segunda iteração encontra uma chave que já existe no dicionário novo e a sobrescreve. Com {"BR": "Brasil", "PT": "Portugal", "AO": "Portugal"}, o invertido tem duas entradas e não três, e o que ficou foi {'Brasil': 'BR', 'Portugal': 'AO'} — sobrou o último a ser processado, porque a compreensão percorre na ordem de inserção e cada repetição apaga a anterior. Sete entradas sumiram exatamente assim, e o silêncio é o problema: não há exceção, não há aviso, e o resultado continua sendo um dicionário perfeitamente válido. A primeira providência, sempre que se inverte, é comparar len(original) com len(invertido) e falhar ruidosamente se diferirem; uma linha de asserção teria encurtado essas semanas para minutos. Quando o objetivo é de fato agrupar — vários códigos para o mesmo nome — o valor da nova chave precisa ser uma coleção, e aí o idiomático é o defaultdict: agrupado = defaultdict(list) seguido de for k, v in codigos.items(): agrupado[v].append(k), que devolve {'Brasil': ['BR'], 'Portugal': ['PT', 'AO']}. Sem importar nada, o equivalente é codigos.setdefault(...) ou, mais explícito, agrupado.setdefault(v, []).append(k). A escolha entre inverter e agrupar não é de estilo: é a diferença entre um índice correto e um que perde linhas.
Exercício 4
Uma rotina de limpeza remove do estoque os itens zerados percorrendo o dicionário e chamando del nas chaves que zeraram. Em teste com um item zerado funcionou; com dois, levantou RuntimeError. Um colega comenta que o mesmo código escrito sobre uma lista nunca deu erro. Explique os dois comportamentos e diga qual dos dois é o pior.
Ver resposta
✓ Resposta: O dicionário mantém um contador de versão e o iterador o confere a cada passo: inserir ou remover chave durante a iteração muda o tamanho, o iterador percebe e levanta RuntimeError: dictionary changed size during iteration. Com um item só, o erro às vezes escapa, porque a remoção pode cair na última volta e o laço termina antes da próxima conferência — daí o teste ter passado. Isso já mostra que o comportamento não é confiável nem quando parece funcionar. A lista não faz essa verificação: ela itera por índice, e remover um elemento faz os seguintes deslizarem uma posição para trás enquanto o índice avança, de modo que um elemento é pulado em silêncio. Removendo os pares de [2, 4, 6] dentro do laço, o resultado é [4], sem erro nenhum. Respondendo ao que foi perguntado: o pior é o da lista, e com folga. O RuntimeError é um defeito que se anuncia, aparece no primeiro teste sério e custa minutos; o da lista devolve resultado errado, passa em revisão, entra em produção e só é descoberto quando alguém confere um número à mão, meses depois. Erro barulhento é característica desejável. A correção é a mesma nos dois casos e tem duas formas: percorrer uma cópia — for fruta in list(estoque):, em que list() materializa as chaves antes de o laço começar — ou, melhor, construir a coleção nova em vez de mutilar a antiga, com estoque = {k: v for k, v in estoque.items() if v > 0}, que diz a intenção e não tem como falhar. Vale a ressalva de que alterar apenas o valor de uma chave já existente é seguro em ambos: o que quebra é mudar o tamanho.
Exercício 5
Uma função recebe um dicionário de configuração, guarda chaves = config.keys() para registrar no log ao final, e no meio do caminho acrescenta valores padrão que faltavam. No log aparecem chaves que não existiam quando a variável foi criada. Além disso, chaves[0] levanta erro. Explique os dois fatos e diga como registrar o estado original.
Ver resposta
✓ Resposta: keys(), values() e items() não devolvem listas: devolvem visões, objetos ligados ao dicionário vivo. A visão não guarda cópia dos dados, apenas aponta para o dicionário, e por isso reflete toda alteração feita depois de ela ter sido criada. Guardando chaves = d.keys() com d = {"a": 1} e inserindo d["b"] = 2 em seguida, imprimir chaves mostra dict_keys(['a', 'b']). Não há bug no log: ele registrou fielmente o estado do dicionário no momento em que foi impresso, que não é o momento que a função pretendia capturar. O segundo fato tem a mesma origem: visão não é sequência, não tem posição, e chaves[0] levanta TypeError: 'dict_keys' object is not subscriptable. Visão suporta len, iteração e teste de pertinência, e ainda se comporta como conjunto — d.keys() & outro.keys() devolve as chaves comuns, o que é bem mais direto do que um laço. Para congelar o estado original, basta materializar na hora: chaves = list(config.keys()), ou set(config) se a ordem não importa. Esse é o mesmo motivo pelo qual for k in list(d) resolve o problema de alterar o dicionário durante a iteração — o list() tira um retrato antes de o laço começar. O desenho das visões não é um capricho: elas evitam copiar um dicionário grande só para percorrê-lo, e o custo dessa economia é justamente que quem precisa de um retrato tem de pedir explicitamente.