Dicionários: chave, valor e as estruturas do mundo real

Dicionários: chave, valor e as estruturas do mundo real

Dicionários trocam posição por significado e são a estrutura mais usada do Python. Veja as bordas onde o código quebra: a cópia rasa que compartilha valores aninhados, o True que sobrescreve a chave 1 sem erro, a inversão que perde entradas e as visões que mudam sozinhas.
Python

• • 20 min de leitura

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

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.

Comentários

Mais em Python

Introdução ao Desenvolvimento Web com Flask
Introdução ao Desenvolvimento Web com Flask

Flask começa com uma rota e uma função, e os erros aparecem no que ele deixa…

Carreira: portfólio, GitHub, open source e o mercado Python
Carreira: portfólio, GitHub, open source e o mercado Python

O último artigo da série de Python: portfólio, README, contribuição open…

Algoritmos de Ordenação e Busca
Algoritmos de Ordenação e Busca

Os algoritmos clássicos de ordenação e busca, com o que cada um ensina a…