TypeIs en Python: refina ambas ramas

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
Close-up of a python snake curled up, showcasing its detailed scales.

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 None

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

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

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec en Python: conserva firmas

    Aprende ParamSpec en Python para conservar firmas completas en decoradores, callbacks, wrappers async y funciones de orden superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard en Python: refina tipos seguros

    Aprende TypeGuard en Python para refinar tipos y validar colecciones, TypedDict, Protocol y datos externos con comprobaciones reales.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    typing.Self en Python: retornos fluidos

    Aprende typing.Self en Python para métodos fluidos, classmethods, builders, clones, Protocol, context managers y retornos que conservan subclases.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup en Python: múltiples errores

    Aprende ExceptionGroup en Python para múltiples errores, except*, grupos anidados, TaskGroup, filtros, logging y validación por lotes.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Picturesque wooden boardwalk leading to a serene beach under clear blue skies.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk en Python: recorre directorios

    Aprende Path.walk en Python para recorrer directorios, podar carpetas, manejar errores y symlinks, calcular tamaños y soportar versiones antiguas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A man in a blue shirt holding a wall clock above his head, contemplating time.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout en Python: controla plazos

    Aprende asyncio.timeout en Python para deadlines, timeout_at, reagendamiento, TaskGroup, cleanup, retries y cancelación asíncrona segura.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026