El refinamiento de tipos permite que un analizador estático transforme una unión amplia en tipos más específicos después de una condición. typing.TypeIs representa predicados que identifican un subtipo real y refinan tanto la rama verdadera como la falsa. Resulta útil cuando un helper reutilizable funciona como prueba de tipo, pero la regla debe quedar centralizada y expresada con términos del dominio.
Esta guía compara TypeIs con TypeGuard e isinstance(), explica uniones, subclases, Protocol, genéricos y valores opcionales, y muestra cómo evitar anotaciones que prometen relaciones imposibles.
Por qué un helper booleano puede perder información
def es_string(valor: object) -> bool:
return isinstance(valor, str)
item: str | bytes = "texto"
if es_string(item):
print(item.upper())La implementación es correcta, pero la firma solo indica que devuelve un booleano. El analizador puede no conectar ese resultado con el tipo de item.
Declarar TypeIs
from typing import TypeIs
def es_string(valor: object) -> TypeIs[str]:
return isinstance(valor, str)Cuando la función devuelve True, el argumento se refina a str. Cuando devuelve False, str se elimina de las posibilidades. Para str | bytes, la rama falsa pasa a ser bytes.
Refinamiento bidireccional
def procesar(valor: str | bytes) -> None:
if es_string(valor):
print(valor.casefold())
else:
print(valor.hex())Esta precisión en la rama negativa es la diferencia práctica principal frente a TypeGuard, cuyo enfoque tradicional es la rama verdadera.
La relación de subtipo es obligatoria
El tipo dentro de TypeIs debe ser compatible como subtipo del parámetro. Un predicado que recibe str | bytes puede identificar str o bytes, pero no debe afirmar que el valor es un int.
def invalido(valor: str | bytes) -> TypeIs[int]:
return isinstance(valor, int)Los analizadores deberían rechazar esta firma. La restricción es lo que permite excluir el subtipo de forma segura en la rama negativa.
Clases base y subclases
from dataclasses import dataclass
from typing import TypeIs
@dataclass
class Evento:
id: int
@dataclass
class EventoError(Evento):
mensaje: str
def es_error(evento: Evento) -> TypeIs[EventoError]:
return isinstance(evento, EventoError)La rama verdadera ve EventoError. La rama falsa excluye esa subclase y conserva las demás posibilidades compatibles con Evento.
Discriminar una unión
@dataclass
class Creado:
recurso_id: int
@dataclass
class Eliminado:
recurso_id: int
@dataclass
class Fallido:
motivo: str
Accion = Creado | Eliminado | Fallido
def es_fallo(valor: Accion) -> TypeIs[Fallido]:
return isinstance(valor, Fallido)Después de if es_fallo(valor), la rama negativa contiene Creado | Eliminado. Una secuencia de predicados puede reducir progresivamente una unión grande.
TypeIs frente a TypeGuard
TypeGuard admite refinamientos más flexibles. Puede declarar, por ejemplo, que un list[object] contiene solo strings aunque list[str] no sea un subtipo normal de list[object] debido a la invariancia. TypeIs exige una relación de subtipo válida.
Elige TypeIs cuando el predicado identifica una parte real del tipo de entrada y necesitas precisión en la rama negativa. Elige TypeGuard para validación estructural o relaciones que el subtipado ordinario no puede representar.
TypeIs frente a isinstance
Para una condición local sencilla, isinstance() sigue siendo más directo.
if isinstance(valor, str):
...TypeIs aporta valor cuando la prueba tiene un nombre de dominio, aparece en varios módulos, combina varias condiciones o esconde detalles de implementación.
Predicados con condiciones adicionales
class Respuesta:
status: int
class RespuestaExito(Respuesta):
datos: dict[str, object]
def es_respuesta_exito(
respuesta: Respuesta,
) -> TypeIs[RespuestaExito]:
return (
isinstance(respuesta, RespuestaExito)
and 200 <= respuesta.status < 300
)Todo valor aceptado debe pertenecer realmente al subtipo declarado. Las condiciones extra pueden seleccionar un subconjunto menor, pero nunca deben permitir un objeto fuera del tipo prometido.
Protocol y runtime_checkable
Protocol expresa contratos estructurales, pero no todos los Protocol admiten isinstance(). Una comprobación de runtime puede usar @runtime_checkable.
from typing import Protocol, TypeIs, runtime_checkable
@runtime_checkable
class Cerrable(Protocol):
def close(self) -> None: ...
def es_cerrable(valor: object) -> TypeIs[Cerrable]:
return isinstance(valor, Cerrable)Las comprobaciones de Protocol en runtime inspeccionan principalmente la presencia de miembros, no una coincidencia profunda de firmas. No sustituyen las pruebas de comportamiento.
TypeIs con genéricos
from collections.abc import Sequence
from typing import TypeIs, TypeVar
T = TypeVar("T")
def es_tupla(valor: Sequence[T]) -> TypeIs[tuple[T, ...]]:
return isinstance(valor, tuple)El predicado conserva el parámetro genérico mientras identifica una implementación específica de la interfaz. El destino sigue siendo compatible con el parámetro original.
Valores opcionales
def no_es_none(valor: T | None) -> TypeIs[T]:
return valor is not NoneLa rama verdadera se convierte en T y la falsa en None. Una comprobación local con is not None suele ser suficiente, pero el predicado reutilizable puede ayudar en APIs funcionales y validación compartida.
Filtrar valores
def es_entero(valor: object) -> TypeIs[int]:
return type(valor) is int
items: list[object] = [1, "a", True, 2]
enteros = [item for item in items if es_entero(item)]La identidad exacta de tipo rechaza booleanos, porque bool es subclase de int. Decide conscientemente entre identidad exacta e isinstance().
Intersección con el tipo conocido
TypeIs no reemplaza ciegamente el tipo anterior. El analizador calcula una intersección entre el tipo conocido y el subtipo declarado. Si una variable ya es str | bytes y el predicado devuelve TypeIs[str], la rama positiva se convierte en str. En casos complejos se conserva la información compatible ya conocida.
Predicados inseguros
def es_string(valor: object) -> TypeIs[str]:
return hasattr(valor, "upper")Un objeto puede definir un método upper sin ser una cadena. Esta implementación produciría un refinamiento falso. El predicado debe aceptar valores del tipo declarado y rechazar valores externos según el contrato.
Compatibilidad de versiones
TypeIs está disponible en versiones modernas de Python. Las bibliotecas que soportan intérpretes anteriores pueden importarlo desde typing_extensions.
try:
from typing import TypeIs
except ImportError:
from typing_extensions import TypeIsDeclara la dependencia mínima y ejecuta el analizador contra todas las versiones soportadas.
TypeIs frente a cast
cast() cambia únicamente la visión del analizador y no ejecuta una prueba. TypeIs depende de un predicado real, devuelve un booleano y puede refinar ambas ramas. Prefiere validación real en fronteras de datos y usa cast solo cuando otro mecanismo ya garantiza la invariante.
Guías de diseño
- Da al predicado un nombre que identifique claramente el subtipo.
- Mantén la función pura y sin efectos secundarios.
- Haz que la implementación coincida exactamente con la anotación.
- Prefiere
isinstance()local cuando no exista reutilización. - Prueba casos positivos, negativos y subclases inesperadas.
- Verifica el resultado con mypy, pyright o el analizador del proyecto.
Ejemplo completo con mensajes de una cola
from dataclasses import dataclass
from typing import TypeIs
@dataclass
class Mensaje:
id: str
@dataclass
class Comando(Mensaje):
nombre: str
argumentos: dict[str, object]
@dataclass
class Evento(Mensaje):
topico: str
MensajeCola = Comando | Evento
def es_comando(mensaje: MensajeCola) -> TypeIs[Comando]:
return isinstance(mensaje, Comando)
def despachar(mensaje: MensajeCola) -> None:
if es_comando(mensaje):
ejecutar(mensaje.nombre, mensaje.argumentos)
else:
publicar(mensaje.topico)El predicado abstrae la clasificación y refina ambas ramas. El despachador no necesita casts ni comentarios para explicar el tipo restante.
Cuándo no usar TypeIs
No uses TypeIs para convertir datos, validar el contenido interno de contenedores invariantes o afirmar un tipo que no sea subtipo de la entrada. Considera TypeGuard, funciones de parsing que devuelven un objeto nuevo, bibliotecas de schemas o validación explícita con excepciones.
Conclusión
typing.TypeIs es la herramienta adecuada para predicados reutilizables que reconocen un subtipo real. Refina la rama verdadera mediante intersección y elimina el subtipo de la rama falsa, produciendo flujos precisos.
La documentación oficial de TypeIs en Python define las reglas. Compárala con la guía de TypeGuard en Python para elegir entre refinamiento bidireccional estricto y validación flexible.







