TypeIs no Python: refine os dois ramos

Publicado em: 28/08/2026
Tempo de leitura: 6 minutos
A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.

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ível

Verificadores 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 None

Esse 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 TypeIs

Declare 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard no Python: refine tipos com segurança

    Aprenda TypeGuard no Python para refinar tipos, validar coleções, TypedDict e Protocol com segurança estática e checagem real.

    Ler mais

    Tempo de leitura: 6 minutos
    28/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

    typing.Self no Python: retornos fluentes

    Aprenda typing.Self no Python para métodos fluentes, classmethods, builders, clones, Protocol, context managers e retornos que preservam subclasses.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Yellow block letters spelling 'error' on a vibrant pink background, capturing a playful message.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup no Python: múltiplos erros

    Aprenda ExceptionGroup no Python para múltiplos erros, except*, grupos aninhados, TaskGroup, filtros, logging e validação em lote.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A tranquil wooden pathway winds through a vibrant autumn forest, covered in fallen leaves.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk no Python: percorra diretórios

    Aprenda Path.walk no Python para percorrer diretórios, podar pastas, tratar erros, links simbólicos, tamanhos, remoção segura e compatibilidade.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Minimalist hourglass filled with sand symbolizing time and patience, against a soft background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout no Python: controle prazos

    Aprenda asyncio.timeout no Python para deadlines, timeout_at, reagendamento, TaskGroup, cleanup, retries e cancelamento assíncrono seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A detailed view of computer programming code on a screen, showcasing software development.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup no Python: concorrência estruturada

    Aprenda asyncio.TaskGroup no Python para concorrência estruturada, resultados, cancelamento, ExceptionGroup, timeouts e tarefas aninhadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026