ReadOnly no Python: proteja campos TypedDict

Publicado em: 29/08/2026
Tempo de leitura: 6 minutos
Detailed view of programming code in a dark theme on a computer screen.

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"] = 99

Para 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ático

O 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: bool

titulo 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 normalmente

Se 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: str

id é 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: str

As 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: str

Nã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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeAliasType no Python: aliases em runtime

    Aprenda TypeAliasType no Python para criar aliases explícitos, inspecionar tipos em runtime e modelar APIs genéricas com segurança.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    overload no Python: assinaturas precisas

    Aprenda typing.overload no Python para criar assinaturas precisas com Literal, None, genéricos, métodos e retornos dependentes dos argumentos.

    Ler mais

    Tempo de leitura: 8 minutos
    29/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ClassVar no Python: separe classe e instância

    Aprenda ClassVar no Python para separar atributos de classe e instância em dataclasses, registries, caches, herança e contadores.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Final no Python: proteja constantes e herança

    Aprenda Final e @final no Python para proteger constantes, atributos, métodos e classes, entendendo os limites em runtime.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Annotated no Python: tipos com metadados

    Aprenda Annotated no Python para adicionar metadados a tipos, criar validação, schemas, unidades e integrações com frameworks.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    Laptop displaying code editor on a desk with a coffee mug beside it, suggesting a workspace or home office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    NewType no Python: IDs sem misturar

    Aprenda NewType no Python para separar IDs, códigos e valores primitivos, validar fronteiras e evitar misturas sem criar classes pesadas.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026