typing.ReadOnly permite marcar chaves de um TypedDict como somente leitura para o analisador estático. O recurso é útil quando uma estrutura de dicionário contém campos que podem ser lidos por consumidores, mas não devem ser alterados depois de criados, como IDs, timestamps, versões, códigos de auditoria e valores derivados por um serviço.
ReadOnly não congela o dicionário em runtime. Ele documenta e verifica um contrato de escrita durante a análise estática. Neste guia, você verá como declarar campos somente leitura, combinar ReadOnly com campos obrigatórios e opcionais, usar herança, entender variância, integrar com APIs e evitar confundir segurança estática com imutabilidade real.
O problema de dicionários mutáveis
from typing import TypedDict
class Usuario(TypedDict):
id: int
nome: str
usuario: Usuario = {"id": 1, "nome": "Ana"}
usuario["id"] = 99Para o Python, essa alteração é normal. Porém, em muitos domínios, o identificador deveria ser definido apenas na criação. Sem uma anotação específica, o analisador não distingue campos editáveis de campos protegidos.
Declarando ReadOnly
from typing import ReadOnly, TypedDict
class Usuario(TypedDict):
id: ReadOnly[int]
nome: str
usuario: Usuario = {"id": 1, "nome": "Ana"}
usuario["nome"] = "Ana Silva" # permitido
usuario["id"] = 2 # erro estáticoO campo id continua sendo um inteiro legível. A diferença é que atribuições, remoções ou outras operações de mutação sobre essa chave devem ser rejeitadas pelo verificador de tipos.
ReadOnly atua por campo
TypedDict não se torna inteiro somente leitura. Você escolhe quais chaves possuem o contrato:
class Registro(TypedDict):
criado_em: ReadOnly[str]
versao: ReadOnly[int]
titulo: str
ativo: booltitulo e ativo permanecem editáveis. Isso permite modelar objetos com identidade estável e estado mutável.
Não há proteção automática em runtime
registro: Registro = {
"criado_em": "2026-07-26",
"versao": 1,
"titulo": "Exemplo",
"ativo": True,
}
registro["versao"] = 10 # Python executa normalmenteSe o código não passa por análise estática, a mutação ocorre. Para imutabilidade real, use MappingProxyType, uma dataclass congelada, uma classe com propriedades sem setter ou uma estrutura persistente. O artigo sobre MappingProxyType no Python mostra uma visão somente leitura de dicionários.
ReadOnly e totalidade
ReadOnly responde à pergunta “pode ser escrito depois?”. A totalidade responde “a chave precisa existir?”. São dimensões diferentes.
from typing import NotRequired, ReadOnly, Required, TypedDict
class Resposta(TypedDict, total=False):
id: Required[ReadOnly[int]]
cache: NotRequired[ReadOnly[str]]
mensagem: strid é obrigatório e somente leitura. cache é opcional e somente leitura quando existe. mensagem é opcional e editável porque a classe usa total=False.
Ordem dos qualificadores
Ferramentas modernas entendem combinações de Required, NotRequired e ReadOnly. Prefira uma forma consistente no projeto e valide com o analisador escolhido. A intenção deve ficar evidente na revisão de código.
Campos calculados
class Pedido(TypedDict):
subtotal: float
desconto: float
total: ReadOnly[float]O campo total pode ser produzido por uma função de fábrica e depois tratado como estável pelos consumidores. Isso reduz alterações acidentais em valores que deveriam ser derivados.
Factories e fronteiras de criação
def criar_pedido(subtotal: float, desconto: float) -> Pedido:
return {
"subtotal": subtotal,
"desconto": desconto,
"total": subtotal - desconto,
}ReadOnly não impede que a factory forneça o valor inicial. O contrato normalmente permite construir o dicionário completo, mas proíbe reatribuição posterior através de uma referência tipada.
Atualizações parciais
APIs de patch não devem reutilizar cegamente o TypedDict completo se alguns campos são somente leitura.
class AtualizarPedido(TypedDict, total=False):
subtotal: float
desconto: float
def atualizar(pedido_id: int, mudancas: AtualizarPedido) -> None:
...Um tipo separado para atualização deixa impossível enviar total ou id pela interface estática. Essa modelagem costuma ser mais clara do que depender apenas de validação em runtime.
Subtipagem e segurança de escrita
Campos somente leitura podem permitir relações de subtipagem mais flexíveis porque o consumidor não pode substituir o valor. Uma estrutura com valor mais específico pode ser lida por uma interface mais geral sem risco de alguém escrever um tipo incompatível.
class Animal: ...
class Cachorro(Animal): ...
class FonteAnimal(TypedDict):
item: ReadOnly[Animal]
class FonteCachorro(TypedDict):
item: ReadOnly[Cachorro]A segurança exata depende das regras do especificador e do verificador. O ponto importante é que proibir escrita reduz os riscos associados à variância.
Herança de TypedDict
class BaseEvento(TypedDict):
id: ReadOnly[str]
criado_em: ReadOnly[str]
class EventoUsuario(BaseEvento):
usuario_id: int
acao: strAs subclasses herdam o contrato somente leitura. Não tente transformar silenciosamente um campo protegido em campo gravável, pois isso quebraria consumidores que confiam na estabilidade da base.
Interfaces de leitura e escrita separadas
Em sistemas maiores, pode ser útil ter um tipo público somente leitura e um tipo interno de construção. A implementação cria ou altera dados internamente, enquanto consumidores recebem uma visão mais restrita.
class UsuarioPublico(TypedDict):
id: ReadOnly[int]
nome: ReadOnly[str]
class UsuarioInterno(TypedDict):
id: int
nome: str
hash_senha: strNão presuma que dois TypedDicts semelhantes são automaticamente intercambiáveis. Confirme as regras de compatibilidade com seu analisador.
ReadOnly em parâmetros
Ao aceitar um TypedDict com campos ReadOnly, a função comunica que não pretende alterar essas chaves:
def exibir(usuario: UsuarioPublico) -> str:
return f"{usuario['id']}: {usuario['nome']}"Isso melhora o contrato, mas funções ainda podem mutar outras chaves editáveis. Para uma interface totalmente de leitura, marque todos os campos ou prefira uma abstração imutável.
ReadOnly e cópias
Criar uma cópia permite produzir um novo valor com um campo diferente, desde que o resultado satisfaça o tipo.
def renomear(usuario: UsuarioPublico, nome: str) -> UsuarioPublico:
return {"id": usuario["id"], "nome": nome}ReadOnly restringe a alteração da chave na referência existente; não proíbe construir outro dicionário.
Serialização
ReadOnly não muda JSON, pickle ou armazenamento. O campo é serializado como qualquer outro. A proteção existe na camada de tipagem e deve ser complementada por validação no servidor quando dados vêm de clientes externos.
APIs HTTP
Um modelo de resposta pode marcar id, created_at e checksum como ReadOnly. Já um modelo de entrada deve simplesmente omitir esses campos. Essa separação evita que clientes tentem controlar valores definidos pelo servidor.
ReadOnly e bibliotecas de validação
Frameworks podem interpretar ReadOnly para documentação ou schemas, mas o comportamento varia. Alguns geram campos apenas de resposta; outros ignoram o qualificador. Verifique a integração e não baseie segurança de runtime apenas na anotação.
Compatibilidade de versões
Em versões que não oferecem typing.ReadOnly, use typing_extensions.ReadOnly. Bibliotecas devem declarar a dependência e testar a mesma base de código com os analisadores suportados.
Erros comuns
- Acreditar que o dicionário foi congelado: ReadOnly é uma regra estática.
- Usar o mesmo tipo para criação, patch e resposta: cada operação pode precisar de um contrato próprio.
- Confundir ReadOnly com NotRequired: escrita e presença são dimensões diferentes.
- Remover uma chave protegida: exclusão também é mutação.
- Depender de frameworks sem verificar suporte: a anotação pode ser ignorada em runtime.
- Redefinir o campo como gravável em herança: isso quebra substituição segura.
Exemplo completo: documento versionado
from typing import ReadOnly, TypedDict
class Documento(TypedDict):
id: ReadOnly[str]
criado_em: ReadOnly[str]
versao: ReadOnly[int]
titulo: str
conteudo: str
def criar_documento(id_: str, titulo: str, conteudo: str) -> Documento:
return {
"id": id_,
"criado_em": "2026-07-26T12:00:00Z",
"versao": 1,
"titulo": titulo,
"conteudo": conteudo,
}
def editar(documento: Documento, titulo: str, conteudo: str) -> Documento:
return {
**documento,
"versao": documento["versao"] + 1,
"titulo": titulo,
"conteudo": conteudo,
}A função não altera o dicionário original. Ela constrói um novo documento com versão incrementada. Dependendo do analisador, a expansão e a substituição de um campo ReadOnly durante a criação de um novo objeto podem exigir uma factory interna ou um tipo de construção separado. O objetivo do modelo é manter referências publicadas estáveis.
Quando escolher outra ferramenta
Use dataclass congelada para objetos realmente imutáveis e com atributos. Use MappingProxyType para uma visão de runtime sem escrita. Use uma classe normal para validação e invariantes. Use ReadOnly quando o formato precisa continuar sendo um dicionário e você quer restringir chaves durante a análise estática.
Conclusão
typing.ReadOnly adiciona uma dimensão importante ao TypedDict: algumas chaves podem ser lidas, mas não reatribuídas por consumidores. Ele melhora contratos de respostas, eventos, registros versionados e objetos com identidade estável.
A documentação oficial de ReadOnly no módulo typing descreve o recurso. Use-o com modelos separados para criação e atualização, execute um verificador estático e implemente proteção de runtime quando a integridade dos dados depender realmente de impedir mutações.







