O refinamento de tipos permite que um analisador estático transforme uma união ampla em tipos mais específicos depois de uma condição. typing.TypeIs foi criado para representar predicados que identificam um subtipo real e refinam tanto o ramo verdadeiro quanto o ramo falso. Ele é especialmente útil quando uma função personalizada equivale a um teste de tipo, mas você quer manter a regra centralizada e reutilizável.
Neste guia, você aprenderá a diferença entre TypeIs, TypeGuard e isinstance(), como escrever predicados corretos, trabalhar com uniões, Protocol, genéricos e subclasses, além de evitar anotações que prometem relações impossíveis.
Um problema comum com funções booleanas
def eh_string(valor: object) -> bool:
return isinstance(valor, str)
entrada: str | bytes = "texto"
if eh_string(entrada):
print(entrada.upper())Mesmo que a lógica esteja correta, um analisador pode não relacionar o retorno booleano com o argumento. A assinatura apenas diz que a função devolve bool.
Declarando TypeIs
from typing import TypeIs
def eh_string(valor: object) -> TypeIs[str]:
return isinstance(valor, str)Quando a função retorna True, o argumento é refinado para str. Quando retorna False, str é removido do conjunto possível. Em uma união str | bytes, o ramo else passa a ser bytes.
Refinamento nos dois ramos
def processar(valor: str | bytes) -> None:
if eh_string(valor):
print(valor.casefold())
else:
print(valor.hex())Esse refinamento bidirecional é a principal diferença prática em relação a TypeGuard, cujo foco tradicional é o ramo verdadeiro.
A relação de subtipo é obrigatória
O tipo indicado em TypeIs precisa ser compatível como subtipo do parâmetro. Se a função recebe str | bytes, ela pode identificar str ou bytes. Não deve afirmar que o valor é int, pois essa possibilidade não pertence ao conjunto original.
def invalido(valor: str | bytes) -> TypeIs[int]:
return isinstance(valor, int) # relação impossívelVerificadores devem rejeitar esse tipo de assinatura. A restrição torna TypeIs mais preciso e seguro para exclusão no ramo negativo.
TypeIs com classes base e subclasses
from dataclasses import dataclass
from typing import TypeIs
@dataclass
class Evento:
id: int
@dataclass
class EventoErro(Evento):
mensagem: str
def eh_erro(evento: Evento) -> TypeIs[EventoErro]:
return isinstance(evento, EventoErro)No ramo verdadeiro, o objeto é EventoErro. No falso, o analisador exclui essa subclasse e mantém as demais possibilidades compatíveis com Evento.
Uniões discriminadas
from dataclasses import dataclass
@dataclass
class Criado:
recurso_id: int
@dataclass
class Removido:
recurso_id: int
@dataclass
class Falhou:
motivo: str
Acao = Criado | Removido | Falhou
def eh_falha(valor: Acao) -> TypeIs[Falhou]:
return isinstance(valor, Falhou)Após if eh_falha(valor), o ramo falso contém Criado | Removido. Isso facilita encadeamentos de condições que reduzem a união progressivamente.
TypeIs e TypeGuard
TypeGuard permite refinamentos mais flexíveis. Por exemplo, ele pode declarar que uma list[object] contém apenas strings, mesmo que list[str] não seja subtipo comum de list[object] devido à invariância. TypeIs exige uma relação de subtipo válida.
Use TypeIs quando o predicado realmente identifica uma parte do tipo de entrada e você quer refinamento no else. Use TypeGuard para validações estruturais ou relações que não cabem no subtipado normal.
TypeIs e isinstance
Para condições locais simples, isinstance() continua sendo a opção mais direta.
if isinstance(valor, str):
...TypeIs vale a pena quando o teste precisa de um nome de domínio, é reutilizado em vários módulos, combina múltiplas verificações ou esconde detalhes de implementação.
Predicados com várias condições
class Resposta:
status: int
class RespostaSucesso(Resposta):
dados: dict[str, object]
def eh_resposta_sucesso(
resposta: Resposta,
) -> TypeIs[RespostaSucesso]:
return (
isinstance(resposta, RespostaSucesso)
and 200 <= resposta.status < 300
)A anotação só é correta se todo valor aceito for realmente uma instância do subtipo. Condições adicionais podem estreitar o conjunto ainda mais, mas nunca podem permitir objetos fora do tipo declarado.
Protocol e runtime_checkable
Protocol descreve contratos estruturais, porém nem todo Protocol pode ser usado com isinstance(). Quando a checagem de runtime for necessária, o protocolo pode usar @runtime_checkable.
from typing import Protocol, TypeIs, runtime_checkable
@runtime_checkable
class Fechavel(Protocol):
def close(self) -> None: ...
def eh_fechavel(valor: object) -> TypeIs[Fechavel]:
return isinstance(valor, Fechavel)A verificação de Protocol em runtime considera principalmente a presença dos membros, não uma validação profunda das assinaturas. Não trate isso como substituto de testes de comportamento.
TypeIs com genéricos
from collections.abc import Sequence
from typing import TypeIs, TypeVar
T = TypeVar("T")
def eh_tupla(valor: Sequence[T]) -> TypeIs[tuple[T, ...]]:
return isinstance(valor, tuple)O predicado preserva o parâmetro genérico e identifica uma implementação específica da interface. O tipo de destino precisa continuar sendo compatível com o parâmetro original.
Valores opcionais
def nao_e_none(valor: T | None) -> TypeIs[T]:
return valor is not NoneEsse helper pode ser útil em filtros e fluxos genéricos. No ramo verdadeiro, o valor é T; no falso, é None. Para verificações locais, is not None normalmente já é suficiente, mas a função reutilizável pode melhorar APIs funcionais.
Filtrando listas
def eh_int(valor: object) -> TypeIs[int]:
return type(valor) is int
itens: list[object] = [1, "a", True, 2]
inteiros = [item for item in itens if eh_int(item)]O uso de type(valor) is int exclui booleanos, pois bool é subclasse de int. Escolha conscientemente entre identidade exata de classe e isinstance().
Interseção com o tipo conhecido
TypeIs não substitui cegamente o tipo anterior. O analisador calcula uma interseção entre o tipo conhecido e o tipo indicado. Se uma variável já é str | bytes e o predicado retorna TypeIs[str], o ramo positivo vira str. Em tipos mais complexos, o resultado preserva informações compatíveis já conhecidas.
Predicados incorretos
def eh_string(valor: object) -> TypeIs[str]:
return hasattr(valor, "upper")Ter um método chamado upper não prova que o valor é str. Um objeto personalizado poderia passar no teste. O resultado seria um refinamento falso. A implementação precisa aceitar todos os valores do tipo declarado e rejeitar os que não pertencem a ele, dentro do contrato pretendido.
Compatibilidade de versões
TypeIs está disponível em versões modernas do módulo typing. Bibliotecas que suportam versões anteriores podem importá-lo de typing_extensions.
try:
from typing import TypeIs
except ImportError:
from typing_extensions import TypeIsDeclare a dependência mínima e execute o analisador com as mesmas versões suportadas pelo pacote.
Diferença para cast
cast() apenas altera a visão do analisador e não executa uma checagem. TypeIs depende de um predicado real, produz um booleano e pode refinar os dois caminhos. Em fronteiras de dados, prefira validação real. Use cast quando a invariável já foi provada por outro mecanismo que o verificador não consegue enxergar.
Boas práticas de design
- Dê ao predicado um nome que expresse claramente o subtipo identificado.
- Mantenha a função pura e sem efeitos colaterais.
- Use uma implementação que corresponda exatamente à promessa.
- Prefira
isinstance()local quando não há reutilização. - Escreva testes positivos, negativos e com subclasses inesperadas.
- Confirme o comportamento com mypy, pyright ou o analisador adotado pelo projeto.
Exemplo completo: mensagens de uma fila
from dataclasses import dataclass
from typing import TypeIs
@dataclass
class Mensagem:
id: str
@dataclass
class Comando(Mensagem):
nome: str
argumentos: dict[str, object]
@dataclass
class Evento(Mensagem):
topico: str
MensagemFila = Comando | Evento
def eh_comando(msg: MensagemFila) -> TypeIs[Comando]:
return isinstance(msg, Comando)
def despachar(msg: MensagemFila) -> None:
if eh_comando(msg):
executar(msg.nome, msg.argumentos)
else:
publicar(msg.topico)A função eh_comando() abstrai a regra e refina ambos os ramos. O código de despacho não precisa de casts nem de comentários para explicar tipos.
Quando não usar TypeIs
Não use TypeIs para converter dados, validar conteúdo interno de contêineres invariantes ou afirmar um tipo que não é subtipo da entrada. Nesses casos, considere TypeGuard, funções de parsing que retornam um novo objeto, classes de schema ou validação explícita com exceções.
Conclusão
typing.TypeIs é a ferramenta adequada para predicados reutilizáveis que reconhecem um subtipo real. Ele refina o ramo verdadeiro pela interseção e remove esse subtipo do ramo falso, produzindo fluxos mais precisos.
A documentação oficial de TypeIs no Python detalha as regras. Compare também com o guia de TypeGuard no Python para escolher conscientemente entre refinamento bidirecional estrito e validação mais flexível.







