Em projetos Python com tipagem estática, é comum receber valores amplos e precisar provar ao analisador que eles pertencem a um tipo mais específico. Testes com isinstance() resolvem muitos casos, mas funções auxiliares personalizadas nem sempre refinam o tipo automaticamente. typing.TypeGuard permite declarar que uma função booleana atua como um predicado de tipo: quando ela retorna True, o verificador entende que o argumento corresponde ao tipo indicado.
Neste guia, você aprenderá a criar TypeGuards seguros, validar listas, dicionários e objetos, combinar refinamento com Protocol, TypedDict e Literal, evitar promessas incorretas e decidir quando um simples isinstance() é suficiente.
O problema do refinamento em funções auxiliares
from typing import Any
def eh_lista_de_str(valor: list[Any]) -> bool:
return all(isinstance(item, str) for item in valor)
dados: list[object] = ["a", "b"]
if eh_lista_de_str(dados):
primeiro = dados[0]
# alguns analisadores ainda veem objectA função retorna um booleano correto em runtime, porém sua assinatura não comunica uma relação de tipagem. O analisador sabe apenas que recebeu list[object] e obteve bool.
Declarando um TypeGuard
from typing import TypeGuard
def eh_lista_de_str(valor: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in valor)
if eh_lista_de_str(dados):
primeiro = dados[0]
print(primeiro.upper())Dentro do bloco verdadeiro, dados é tratado como list[str]. O TypeGuard aparece no tipo de retorno, mas a função continua devolvendo apenas True ou False em runtime.
TypeGuard é uma promessa
O verificador não executa sua função para confirmar se a lógica é correta. Ele confia na assinatura. Uma implementação defeituosa pode produzir uma falsa sensação de segurança.
def eh_int(valor: object) -> TypeGuard[int]:
return True # incorretoDepois dessa chamada, o analisador permitirá operações de inteiro mesmo quando o valor for uma string, lista ou objeto arbitrário. Trate TypeGuard como uma fronteira de confiança e teste seus predicados cuidadosamente.
Refinando uniões
from dataclasses import dataclass
from typing import TypeGuard
@dataclass
class Sucesso:
valor: str
@dataclass
class Falha:
erro: str
Resultado = Sucesso | Falha
def foi_sucesso(resultado: Resultado) -> TypeGuard[Sucesso]:
return isinstance(resultado, Sucesso)
def processar(resultado: Resultado) -> None:
if foi_sucesso(resultado):
print(resultado.valor)
else:
print(resultado.erro)Esse padrão deixa o código chamador expressivo e centraliza a regra de identificação.
Validando TypedDict
Dados de JSON e APIs frequentemente chegam como dict[str, object]. Um TypeGuard pode validar a estrutura antes de permitir acesso tipado.
from typing import TypedDict, TypeGuard
class Usuario(TypedDict):
id: int
nome: str
ativo: bool
def eh_usuario(valor: object) -> TypeGuard[Usuario]:
if not isinstance(valor, dict):
return False
return (
isinstance(valor.get("id"), int)
and isinstance(valor.get("nome"), str)
and isinstance(valor.get("ativo"), bool)
)O predicado precisa validar todas as chaves obrigatórias e seus tipos. Apenas verificar a existência de uma chave não é suficiente quando o TypeGuard promete a estrutura completa.
Campos opcionais e chaves extras
Se o TypedDict possui campos opcionais, valide os campos obrigatórios e, quando os opcionais existirem, confirme seus tipos. Chaves extras podem ser aceitas ou rejeitadas conforme o contrato da aplicação. Documente essa decisão, pois o tipo estático não descreve sozinho todas as políticas de validação de runtime.
TypeGuard com Protocol
Protocol define contratos estruturais. Um TypeGuard pode identificar objetos que oferecem determinados atributos e métodos.
from typing import Protocol, TypeGuard
class Gravavel(Protocol):
def salvar(self) -> None: ...
def eh_gravavel(valor: object) -> TypeGuard[Gravavel]:
return callable(getattr(valor, "salvar", None))Esse teste confirma apenas que existe algo chamável chamado salvar. Ele não verifica a assinatura completa. Para contratos críticos, prefira classes abstratas, validação explícita ou testes adicionais.
Listas homogêneas
from collections.abc import Sequence
def eh_sequencia_de_int(
valores: Sequence[object],
) -> TypeGuard[Sequence[int]]:
return all(isinstance(valor, int) for valor in valores)Usar interfaces somente leitura, como Sequence, reduz riscos relacionados à mutabilidade. Refinar uma lista mutável para um tipo mais estreito pode ser perigoso se outra referência inserir um valor incompatível depois da validação.
Mutabilidade e invariância
list é invariável. Um list[str] não é automaticamente um list[object] para fins de escrita segura, pois alguém poderia inserir um inteiro. TypeGuard permite relações que não seriam aceitas por subtipagem comum, portanto a implementação e o uso devem preservar as invariantes do contêiner.
Quando a função apenas lê os valores, anotar o parâmetro como Sequence[object] costuma ser mais seguro. Se o código pretende modificar a coleção após o refinamento, controle quem possui referências mutáveis.
Filtrando coleções
def eh_string(valor: object) -> TypeGuard[str]:
return isinstance(valor, str)
itens: list[object] = ["a", 1, "b"]
textos = [item for item in itens if eh_string(item)]Verificadores modernos conseguem inferir list[str] para a compreensão. Em APIs de ordem superior, como filter(), o resultado pode variar conforme o analisador e os stubs utilizados.
Combinando com Literal
from typing import Literal, TypeGuard
Modo = Literal["leitura", "escrita"]
def eh_modo(valor: str) -> TypeGuard[Modo]:
return valor in {"leitura", "escrita"}Isso é útil ao validar opções vindas de linha de comando, variáveis de ambiente ou arquivos de configuração. O guia sobre Literal no Python mostra como valores exatos melhoram overloads e APIs tipadas.
TypeGuard genérico
Alguns predicados podem preservar relações genéricas.
from typing import TypeGuard, TypeVar
T = TypeVar("T")
def sem_none(valores: list[T | None]) -> TypeGuard[list[T]]:
return all(valor is not None for valor in valores)Depois da validação, o analisador pode tratar a lista como list[T]. Novamente, a mutabilidade exige cuidado: outra referência ainda pode inserir None.
Refinamento negativo
TypeGuard foi projetado principalmente para o ramo verdadeiro. Dependendo do caso, o ramo else pode não ser refinado de forma precisa. Para refinamento nos dois ramos, versões modernas do Python oferecem TypeIs, que expressa uma relação mais próxima da interseção real entre tipos.
Use TypeGuard quando você precisa de flexibilidade para estreitar para um tipo que não é necessariamente subtipo estrito do parâmetro. Use TypeIs quando o predicado representa uma identificação de subtipo válida em ambos os ramos.
Diferença para cast
from typing import cast
usuario = cast(Usuario, dados)cast() não valida nada em runtime. Ele apenas instrui o analisador a confiar no programador. TypeGuard combina uma checagem real com refinamento estático. Prefira TypeGuard em fronteiras externas; reserve cast para situações em que a invariável já foi garantida por outro mecanismo.
Diferença para isinstance
Quando o alvo é uma classe concreta ou uma tupla de classes, isinstance() já costuma refinar corretamente. TypeGuard é útil quando a regra envolve estrutura interna, conteúdo de coleções, combinações de atributos ou validações de domínio.
Testando o predicado
Crie testes positivos e negativos, incluindo valores limítrofes, subclasses, coleções vazias, chaves ausentes e tipos parecidos. Para um TypeGuard de dicionário, teste booleanos onde inteiros são esperados, pois bool é subclasse de int em Python. Caso isso seja indesejado, use type(valor) is int.
Erros comuns
- Retornar True sem validação completa: o tipo prometido pode ser falso.
- Esquecer mutabilidade: a coleção pode mudar depois da checagem.
- Usar TypeGuard onde isinstance basta: aumenta a complexidade sem benefício.
- Validar apenas algumas chaves: um TypedDict incompleto não cumpre o contrato.
- Confundir com conversão: TypeGuard não transforma o valor.
- Esperar refinamento perfeito no else: isso depende da relação e do analisador.
Exemplo completo: validação de resposta de API
from typing import TypedDict, TypeGuard
class Produto(TypedDict):
id: int
nome: str
preco: float
def eh_produto(valor: object) -> TypeGuard[Produto]:
if not isinstance(valor, dict):
return False
id_ = valor.get("id")
nome = valor.get("nome")
preco = valor.get("preco")
return (
type(id_) is int
and isinstance(nome, str)
and isinstance(preco, (int, float))
and not isinstance(preco, bool)
)
def carregar(payload: object) -> Produto:
if not eh_produto(payload):
raise ValueError("produto inválido")
return payloadA função carregar() devolve um Produto tipado somente depois da validação. Em aplicações reais, considere mensagens de erro detalhadas ou bibliotecas de schemas quando precisar explicar múltiplas falhas.
Boas práticas
Mantenha predicados pequenos, puros e determinísticos. Dê nomes que expressem claramente a condição. Faça a assinatura refletir o conjunto real de entradas. Evite efeitos colaterais durante a validação. Centralize regras de fronteira e combine testes de runtime com mypy, pyright ou outro analisador no pipeline de integração contínua.
Conclusão
typing.TypeGuard conecta validação booleana e refinamento estático. Ele é especialmente útil para dados externos, coleções homogêneas, TypedDict, Protocol e regras de domínio que não podem ser expressas apenas com isinstance().
A documentação oficial de TypeGuard no módulo typing descreve a semântica formal. Use a ferramenta como uma promessa auditável: valide tudo que o tipo declara, teste casos adversos e prefira interfaces imutáveis quando o refinamento envolver coleções.







