typing.Required e typing.NotRequired permitem controlar a presença de cada chave em um TypedDict. Eles resolvem um problema comum: uma estrutura pode ter algumas chaves obrigatórias e outras opcionais, independentemente do valor aceitar None.
Neste guia, você aprenderá a diferença entre presença e nulabilidade, como combinar campos em classes total=True e total=False, como modelar payloads de criação, atualização e resposta, como usar herança, ReadOnly, Unpack e validação de runtime, além dos erros que tornam contratos de dicionário ambíguos.
O que total controla
Por padrão, todas as chaves declaradas em um TypedDict são obrigatórias:
from typing import TypedDict
class Usuario(TypedDict):
id: int
nome: str
usuario: Usuario = {"id": 1, "nome": "Ana"}Se nome estiver ausente, o analisador deve sinalizar erro. Quando usamos total=False, todas as chaves ficam opcionais:
class AtualizacaoUsuario(TypedDict, total=False):
nome: str
email: strEsse modelo é adequado para patches, pois o cliente pode enviar apenas o campo que deseja alterar.
Required em um TypedDict parcial
from typing import Required, TypedDict
class Evento(TypedDict, total=False):
id: Required[str]
origem: Required[str]
detalhes: dict[str, object]Mesmo com total=False, id e origem precisam existir. detalhes pode estar ausente. Required sobrescreve a regra geral da classe para uma chave específica.
NotRequired em um TypedDict total
from typing import NotRequired, TypedDict
class Produto(TypedDict):
id: int
nome: str
descricao: NotRequired[str]id e nome são obrigatórios; descricao pode não existir. NotRequired permite manter a classe total sem criar uma base separada apenas para um campo opcional.
Ausente não é igual a None
class Perfil(TypedDict):
apelido: NotRequired[str]
biografia: str | Noneapelido pode estar ausente. Se existir, deve ser string. biografia precisa existir, mas seu valor pode ser string ou None. Essa distinção é fundamental para JSON, bancos de dados e APIs de patch.
Três estados diferentes
Um campo pode ter três situações: ausente, presente com valor e presente com None. Nem toda API precisa dos três estados, mas quando precisa, modele-os explicitamente.
class AtualizarPerfil(TypedDict, total=False):
apelido: str
biografia: str | NoneA ausência de biografia significa “não alterar”. O valor None significa “limpar a biografia”. Uma string significa “definir novo conteúdo”.
Verificando chaves opcionais
def aplicar(perfil: dict[str, object], patch: AtualizarPerfil) -> None:
if "apelido" in patch:
perfil["apelido"] = patch["apelido"]
if "biografia" in patch:
perfil["biografia"] = patch["biografia"]O teste com in informa ao analisador que uma chave NotRequired está disponível naquele ramo. Acessar diretamente uma chave opcional pode gerar aviso ou risco de KeyError.
get e valores padrão
dict.get() evita KeyError, mas pode misturar ausência e None:
valor = patch.get("biografia")Se a semântica distingue “não enviado” de “enviado como None”, use in ou uma sentinela:
AUSENTE = object()
valor = patch.get("biografia", AUSENTE)Modelos separados por operação
Uma API robusta costuma usar tipos diferentes:
class CriarUsuario(TypedDict):
nome: str
email: str
telefone: NotRequired[str]
class AtualizarUsuario(TypedDict, total=False):
nome: str
email: str
telefone: str | None
class UsuarioResposta(TypedDict):
id: int
nome: str
email: str
telefone: str | NoneO modelo de criação exige os dados mínimos. O patch aceita subconjuntos. A resposta garante campos produzidos pelo servidor.
Herança para compartilhar campos
class Identidade(TypedDict):
id: int
class DadosPublicos(TypedDict, total=False):
apelido: str
avatar: str
class UsuarioCompleto(Identidade, DadosPublicos):
nome: strHerança pode combinar grupos de chaves com totalidades diferentes. Porém, hierarquias complexas podem ficar difíceis de entender. Prefira poucas camadas e documente a semântica.
Required e ReadOnly
from typing import ReadOnly
class Registro(TypedDict, total=False):
id: Required[ReadOnly[int]]
criado_em: Required[ReadOnly[str]]
nota: strid e criado_em precisam existir e não devem ser reatribuídos por consumidores. Presença e escrita continuam sendo dimensões independentes. Consulte o guia sobre ReadOnly no Python.
Required em construtores dinâmicos
Ao montar dicionários em etapas, o analisador pode não conseguir provar que todas as chaves obrigatórias foram adicionadas:
dados = {}
dados["id"] = 1
dados["nome"] = "Ana"
# atribuir dados a Usuario pode falhar na análisePrefira literais completos, factories tipadas ou uma variável anotada desde o início. Casts devem ser usados apenas quando a validação já ocorreu.
Validação de runtime
TypedDict e seus qualificadores não validam dados externos. Um JSON sem chave obrigatória continua sendo um dicionário em runtime. Valide as fronteiras:
def eh_usuario(valor: object) -> bool:
if not isinstance(valor, dict):
return False
return (
isinstance(valor.get("id"), int)
and isinstance(valor.get("nome"), str)
)Para refinar o tipo depois da validação, use TypeGuard ou TypeIs. O artigo sobre TypeGuard no Python mostra esse padrão.
APIs e OpenAPI
Frameworks podem traduzir Required e NotRequired em campos obrigatórios ou opcionais de um schema. O suporte varia, especialmente com herança e combinações de qualificadores. Confirme o schema gerado e mantenha testes de contrato.
Unpack em **kwargs
from typing import Unpack
class Opcoes(TypedDict, total=False):
timeout: float
cache: bool
def executar(**opcoes: Unpack[Opcoes]) -> None:
...Required e NotRequired controlam quais argumentos nomeados são exigidos quando um TypedDict é usado com Unpack. Um guia dedicado sobre typing.Unpack complementa este tema.
Compatibilidade entre TypedDicts
Uma estrutura com uma chave obrigatória nem sempre é compatível com outra que considera a mesma chave opcional. O consumidor do tipo opcional pode remover a chave ou operar de forma incompatível. As regras consideram presença, escrita, tipo do valor e herança.
Campos reservados e sintaxe funcional
Quando uma chave não pode ser escrita como identificador de classe, use a sintaxe funcional:
Cabecalhos = TypedDict(
"Cabecalhos",
{
"content-type": Required[str],
"x-request-id": NotRequired[str],
},
)Essa forma também ajuda quando as chaves são geradas ou contêm hífens.
Introspecção
TypedDicts expõem conjuntos de chaves obrigatórias e opcionais:
print(Produto.__required_keys__)
print(Produto.__optional_keys__)Frameworks podem usar essas informações, mas referências futuras e avaliação de anotações podem exigir cuidado. Não use introspecção como substituto de validação completa.
total não conta toda a história
O atributo __total__ informa apenas o valor declarado no corpo atual. Uma classe pode ter __total__ == True e ainda possuir chaves NotRequired ou herdadas de uma base parcial. Para descobrir presença real, use os conjuntos de chaves.
Migração de contratos
Transformar uma chave obrigatória em opcional costuma ser compatível para produtores, mas pode afetar consumidores que acessavam sem teste. Tornar uma chave opcional obrigatória pode quebrar todos os payloads antigos. Faça versões de schema, validação gradual ou valores padrão.
Erros comuns
- Usar Optional para indicar ausência:
T | Nonecontrola o valor, não a presença. - Acessar NotRequired sem verificar: pode ocorrer KeyError.
- Reutilizar o modelo de resposta como patch: exige campos que não devem ser enviados.
- Confiar em TypedDict em runtime: dados externos ainda precisam de validação.
- Criar heranças profundas: a totalidade real fica difícil de enxergar.
- Usar get quando ausência e None são diferentes: a semântica pode ser perdida.
Exemplo completo: configuração de tarefa
from typing import NotRequired, Required, TypedDict
class Tarefa(TypedDict, total=False):
nome: Required[str]
comando: Required[list[str]]
diretorio: str
timeout: float
tentativas: int
ambiente: dict[str, str]
descricao: NotRequired[str]
def executar(tarefa: Tarefa) -> None:
nome = tarefa["nome"]
comando = tarefa["comando"]
diretorio = tarefa.get("diretorio", ".")
timeout = tarefa.get("timeout", 30.0)
tentativas = tarefa.get("tentativas", 1)
print(nome, comando, diretorio, timeout, tentativas)As duas chaves essenciais são Required. As demais podem ser omitidas e receber padrões. O contrato continua legível sem dividir a estrutura em várias classes.
Boas práticas
Modele presença separadamente de nulabilidade. Crie tipos diferentes para criar, atualizar e responder. Prefira in quando ausência tem significado. Valide dados nas fronteiras. Execute mypy, pyright ou outro analisador no CI. Revise schemas gerados por frameworks e mantenha testes com payloads mínimos, completos e inválidos.
Conclusão
Required e NotRequired tornam TypedDicts mais expressivos ao controlar a presença de cada chave. Eles permitem representar payloads reais sem confundir ausência com None e sem forçar todas as chaves a seguir a mesma totalidade.
A documentação oficial de Required e NotRequired no módulo typing detalha as regras. Use os qualificadores para contratos estáticos claros e complemente-os com validação de runtime em qualquer entrada não confiável.







