---
title: "frozendict no Python 3.15: Dicionário Imutável"
url: "https://python.dev.br/blog/frozendict-python-3-15-dicionario-imutavel/"
markdown_url: "https://python.dev.br/blog/frozendict-python-3-15-dicionario-imutavel.MD"
description: "Conheça o frozendict do Python 3.15 (PEP 814): dicionário imutável e hashable nativo, com exemplos práticos de uso em cache, sets e thread safety."
date: "2026-04-10"
author: "Equipe python.dev.br"
---

# frozendict no Python 3.15: Dicionário Imutável

Conheça o frozendict do Python 3.15 (PEP 814): dicionário imutável e hashable nativo, com exemplos práticos de uso em cache, sets e thread safety.


Python sempre teve `frozenset` como versão imutável de `set`, mas nunca ofereceu o equivalente para dicionários. Isso muda no **Python 3.15** com a aprovação da **PEP 814**, que adiciona `frozendict` como tipo **built-in** — sem necessidade de instalar pacotes externos.

O `frozendict` é um dicionário imutável e hashable que pode ser usado como chave de outros dicionários, elemento de conjuntos e compartilhado entre threads sem preocupação com mutações acidentais. É uma adição que profissionais de Python pediam há mais de uma década.

## Por que Python precisava de um frozendict?

Cenários onde dicionários imutáveis fazem falta são comuns no dia a dia:

- **Configurações constantes** que não devem ser alteradas após a inicialização
- **Chaves compostas** em dicionários ou caches (dicts não são hashable)
- **Programação funcional** onde imutabilidade evita efeitos colaterais
- **Thread safety** sem locks — se o dado não muda, não há race condition
- **Caching** com `functools.lru_cache` que exige argumentos hashable

Até agora, a solução era usar `types.MappingProxyType` (somente leitura, mas não hashable) ou bibliotecas de terceiros. Nenhuma era ideal.

Se você está revisando conceitos de estruturas de dados em Python, vale conferir nosso artigo sobre [dicionários](/glossario/dicionario/) e o guia completo de [estruturas de dados](/blog/estruturas-de-dados-python/).

## Criando um frozendict

A API do construtor é idêntica à de `dict`:

```python
# Vazio
vazio = frozendict()

# A partir de keyword arguments
config = frozendict(host="localhost", porta=5432, db="meubanco")

# A partir de um dict existente
dados = {"nome": "Maria", "idade": 30}
imutavel = frozendict(dados)

# A partir de iterável de pares
pares = frozendict([("a", 1), ("b", 2), ("c", 3)])

# Combinando dict + kwargs
completo = frozendict({"x": 10}, y=20, z=30)
```

O `frozendict` preserva a **ordem de inserção**, assim como `dict` desde o Python 3.7.

## Operações suportadas

O `frozendict` implementa a interface `collections.abc.Mapping`, então todas as operações de leitura funcionam normalmente:

```python
config = frozendict(host="localhost", porta=5432, ssl=True)

# Acesso por chave
print(config["host"])       # 'localhost'
print(config.get("ssl"))    # True
print(config.get("timeout", 30))  # 30

# Iteração
for chave in config:
    print(chave)
# host, porta, ssl

# Verificação de pertencimento
print("porta" in config)    # True

# Tamanho
print(len(config))          # 3

# Views
print(list(config.keys()))    # ['host', 'porta', 'ssl']
print(list(config.values()))  # ['localhost', 5432, True]
print(list(config.items()))   # [('host', 'localhost'), ('porta', 5432), ('ssl', True)]
```

## O que NÃO funciona (e é proposital)

Todas as operações de mutação lançam `TypeError`:

```python
config = frozendict(host="localhost", porta=5432)

config["host"] = "remoto"      # TypeError
del config["porta"]            # TypeError
config.update(ssl=True)        # AttributeError — método não existe
config.pop("host")             # AttributeError
config.clear()                 # AttributeError
config.setdefault("timeout")   # AttributeError
config.popitem()               # AttributeError
```

Isso é garantido pelo design: `frozendict` **não herda de `dict`**, então não há como burlar a imutabilidade via métodos herdados.

## Hashabilidade: a grande vantagem

A principal diferença prática entre `frozendict` e um `dict` somente leitura é que `frozendict` é **hashable** (quando todos os valores também são hashable):

```python
# Usar como chave de dicionário
permissoes = {
    frozendict(role="admin", nivel=3): ["ler", "escrever", "deletar"],
    frozendict(role="editor", nivel=2): ["ler", "escrever"],
    frozendict(role="leitor", nivel=1): ["ler"],
}

# Usar em conjuntos (sets)
configs_unicas = {
    frozendict(env="prod", regiao="us-east"),
    frozendict(env="prod", regiao="eu-west"),
    frozendict(env="staging", regiao="us-east"),
}
```

O hash é calculado de forma **independente da ordem de inserção**:

```python
a = frozendict(x=1, y=2)
b = frozendict(y=2, x=1)

print(a == b)            # True
print(hash(a) == hash(b))  # True
```

Internamente, o hash equivale a `hash(frozenset(fd.items()))`.

Se valores não hashable estiverem presentes, `hash()` levanta `TypeError`:

```python
fd = frozendict(dados=[1, 2, 3])  # Criação funciona (listas como valor)
hash(fd)  # TypeError: unhashable type: 'list'
```

Para entender melhor hashabilidade em Python, veja nosso glossário sobre [sets](/glossario/set/) e [tuplas](/glossario/tupla/).

## Operador de merge ( | )

O `frozendict` suporta os operadores `|` e `|=`, seguindo o mesmo padrão de `dict` desde o Python 3.9:

```python
base = frozendict(host="localhost", porta=5432)
extra = frozendict(porta=3306, db="mysql")

# Merge cria novo frozendict (chaves duplicadas usam o valor da direita)
resultado = base | extra
print(resultado)  # frozendict({'host': 'localhost', 'porta': 3306, 'db': 'mysql'})

# Funciona com dict normal no lado direito
resultado2 = base | {"ssl": True}
print(resultado2)  # frozendict({'host': 'localhost', 'porta': 5432, 'ssl': True})
```

O operador `|=` **não muta** o frozendict — ele reatribui a variável:

```python
config = frozendict(debug=False)
config |= frozendict(verbose=True)
# config agora aponta para um NOVO frozendict
print(config)  # frozendict({'debug': False, 'verbose': True})
```

## Type annotations

O `frozendict` suporta anotação genérica direta, sem precisar de `typing`:

```python
def carregar_config() -> frozendict[str, str | int]:
    return frozendict(host="localhost", porta=5432)

config: frozendict[str, int] = frozendict(timeout=30, retries=3)
```

Isso se integra bem com ferramentas de tipagem como mypy e o [ty](/blog/ty-type-checker-python-rust/). Para mais sobre tipagem em Python, veja nosso artigo sobre [type hints](/glossario/type-hints/) e [tipagem estática com mypy](/blog/tipagem-estatica-python-mypy/).

## Casos de uso práticos

### Configurações imutáveis de aplicação

```python
from functools import lru_cache

DB_CONFIG = frozendict(
    host="db.producao.interno",
    porta=5432,
    database="app_principal",
    ssl=True,
)

CACHE_CONFIG = frozendict(
    host="redis.producao.interno",
    porta=6379,
    ttl=3600,
)

def conectar_db(config: frozendict[str, str | int | bool]):
    """Conexão segura — config não pode ser alterada acidentalmente."""
    print(f"Conectando em {config['host']}:{config['porta']}")
```

### Cache com chaves compostas

```python
from functools import lru_cache

@lru_cache(maxsize=256)
def buscar_dados(filtros: frozendict[str, str]) -> list:
    """Cache funciona porque frozendict é hashable."""
    print(f"Consultando banco com filtros: {filtros}")
    # ... query real aqui
    return []

# Chamadas com mesmos filtros usam cache
filtro = frozendict(status="ativo", regiao="sudeste")
buscar_dados(filtro)  # Executa query
buscar_dados(filtro)  # Retorna do cache
```

Antes do `frozendict`, esse padrão exigia converter o dict para `tuple(sorted(d.items()))` — feio e propenso a bugs. Para mais sobre decoradores e caching, veja nosso guia de [decoradores em Python](/blog/decoradores-python-guia-pratico/).

### Thread safety sem locks

```python
import threading

# Configuração compartilhada entre threads — segura por ser imutável
SHARED_CONFIG = frozendict(
    workers=4,
    timeout=30,
    max_retries=3,
)

def worker(config: frozendict):
    """Cada thread lê a config sem risco de race condition."""
    print(f"Worker usando timeout={config['timeout']}")

threads = [
    threading.Thread(target=worker, args=(SHARED_CONFIG,))
    for _ in range(SHARED_CONFIG["workers"])
]
for t in threads:
    t.start()
for t in threads:
    t.join()
```

Para mais sobre concorrência em Python, confira nosso artigo sobre [threading](/glossario/threading/) e [multiprocessing](/blog/python-multiprocessing/).

### Pattern matching com frozendict

O `frozendict` funciona com [match/case](/blog/pattern-matching-python-match-case/) usando a sintaxe de mapping:

```python
def processar_evento(evento: frozendict):
    match evento:
        case {"tipo": "login", "usuario": usuario}:
            print(f"Login de {usuario}")
        case {"tipo": "erro", "codigo": codigo}:
            print(f"Erro {codigo}")
        case _:
            print("Evento desconhecido")

evento = frozendict(tipo="login", usuario="maria@exemplo.com")
processar_evento(evento)  # Login de maria@exemplo.com
```

## Comparação com alternativas existentes

| Aspecto | `dict` | `MappingProxyType` | `frozendict` (3.15) |
|---|---|---|---|
| Mutável | Sim | Não* | Não |
| Hashable | Não | Não | Sim |
| Built-in | Sim | `types` module | Sim |
| Chave de dict | Não | Não | Sim |
| Elemento de set | Não | Não | Sim |
| Thread-safe (leitura) | Race conditions possíveis | Sim | Sim |
| Union operator `\|` | Sim | Não | Sim |

*`MappingProxyType` é uma view somente leitura de um dict, mas o dict original ainda pode ser mutado.

## Igualdade com dict

`frozendict` e `dict` são comparáveis entre si:

```python
fd = frozendict(a=1, b=2)
d = {"a": 1, "b": 2}

print(fd == d)   # True
print(d == fd)   # True
```

Isso facilita migração gradual: você pode substituir `dict` por `frozendict` em configurações sem quebrar comparações existentes.

## Quando usar frozendict

**Use frozendict quando:**
- A estrutura não deve mudar após a criação
- Precisa de um mapping como chave de dicionário ou em sets
- Quer cache com `lru_cache` usando dicts como argumento
- Compartilha dados entre threads sem locks
- Quer sinalizar no código que o dado é constante

**Continue usando dict quando:**
- Precisa modificar o conteúdo
- Performance de escrita é crítica
- Está trabalhando com dados temporários que mudam frequentemente

## Conclusão

O `frozendict` é uma adição que deveria ter chegado ao Python há muito tempo. Assim como `frozenset` completou `set` e `tuple` completou `list`, agora `frozendict` completa `dict`.

A aprovação da PEP 814 pelo Steering Council mostra que a comunidade reconhece o valor de tipos imutáveis nativos. Para quem trabalha com configurações, caching, programação funcional ou sistemas concorrentes, essa é provavelmente a adição mais prática do Python 3.15 — junto com as [d-strings](/blog/python-d-strings-pep-822-dedent/) e os [lazy imports](/blog/python-3-15-lazy-imports/).

Fique de olho no [blog](/blog/) para mais novidades sobre Python 3.15 e outras atualizações do ecossistema.

Se o conceito de imutabilidade por padrão te interessa, vale conhecer <a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a>, onde variáveis são imutáveis por padrão e o sistema de tipos garante thread safety em tempo de compilação — a filosofia que inspirou muitas das melhorias recentes do Python.
