En proyectos Python con tipado estático, el código suele comenzar con valores amplios y necesita demostrar que pertenecen a un tipo más específico. Las comprobaciones con isinstance() resuelven muchos casos, pero una función auxiliar personalizada no siempre refina el tipo automáticamente. typing.TypeGuard permite declarar que una función booleana es un predicado de tipo: cuando devuelve True, el analizador trata el argumento como el tipo indicado.
En esta guía aprenderás a crear TypeGuards seguros, validar listas, diccionarios y objetos, combinarlos con Protocol, TypedDict y Literal, evitar promesas incorrectas y decidir cuándo basta una comprobación normal con isinstance().
Por qué un helper booleano no siempre refina
from typing import Any
def es_lista_de_str(valor: list[Any]) -> bool:
return all(isinstance(item, str) for item in valor)
datos: list[object] = ["a", "b"]
if es_lista_de_str(datos):
primero = datos[0]
# el analizador puede seguir viendo objectLa función es correcta en runtime, pero su firma solo comunica que recibe una lista y devuelve un booleano. No describe la relación entre el resultado verdadero y el tipo del argumento.
Declarar un TypeGuard
from typing import TypeGuard
def es_lista_de_str(valor: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in valor)
if es_lista_de_str(datos):
print(datos[0].upper())Dentro de la rama verdadera, datos se trata como list[str]. En runtime, la función sigue devolviendo únicamente True o False; TypeGuard afecta al análisis estático.
TypeGuard es una promesa de confianza
El verificador no ejecuta el predicado para confirmar su lógica. Confía en la anotación. Una implementación defectuosa puede hacer que operaciones inseguras parezcan válidas.
def es_entero(valor: object) -> TypeGuard[int]:
return True # incorrectoDespués de una respuesta verdadera, el analizador permitirá operaciones de entero incluso si el valor es una cadena, una lista u otro objeto. Trata TypeGuard como una frontera de confianza y prueba cada predicado con cuidado.
Refinar una unión
from dataclasses import dataclass
from typing import TypeGuard
@dataclass
class Exito:
valor: str
@dataclass
class Fallo:
error: str
Resultado = Exito | Fallo
def fue_exito(resultado: Resultado) -> TypeGuard[Exito]:
return isinstance(resultado, Exito)
def procesar(resultado: Resultado) -> None:
if fue_exito(resultado):
print(resultado.valor)
else:
print(resultado.error)Este patrón da al código llamador un predicado expresivo del dominio y centraliza la regla de identificación.
Validar un TypedDict
Los datos JSON y de APIs suelen llegar como dict[str, object]. Un TypeGuard puede validar la estructura antes de permitir acceso tipado a las claves.
from typing import TypedDict, TypeGuard
class Usuario(TypedDict):
id: int
nombre: str
activo: bool
def es_usuario(valor: object) -> TypeGuard[Usuario]:
if not isinstance(valor, dict):
return False
return (
isinstance(valor.get("id"), int)
and isinstance(valor.get("nombre"), str)
and isinstance(valor.get("activo"), bool)
)El predicado debe comprobar todas las claves obligatorias y sus tipos. Verificar solo una clave no es suficiente cuando la anotación promete la estructura completa.
Campos opcionales y claves adicionales
Si el TypedDict tiene campos opcionales, valida las claves obligatorias y comprueba los campos opcionales cuando existan. Las claves extra pueden aceptarse o rechazarse según el contrato de la aplicación. Documenta esa política, porque el tipo estático por sí solo no expresa todas las reglas de validación en runtime.
TypeGuard con Protocol
Protocol describe comportamiento estructural. Un TypeGuard puede identificar objetos que exponen determinados atributos o métodos.
from typing import Protocol, TypeGuard
class Guardable(Protocol):
def guardar(self) -> None: ...
def es_guardable(valor: object) -> TypeGuard[Guardable]:
return callable(getattr(valor, "guardar", None))Esta prueba confirma únicamente que existe un atributo invocable llamado guardar. No inspecciona por completo la firma. Para contratos críticos, usa validación explícita, clases abstractas o pruebas más fuertes.
Secuencias homogéneas
from collections.abc import Sequence
def es_secuencia_de_int(
valores: Sequence[object],
) -> TypeGuard[Sequence[int]]:
return all(isinstance(valor, int) for valor in valores)Las interfaces de solo lectura, como Sequence, reducen riesgos de mutación. Refinar una lista mutable puede ser peligroso si otra referencia inserta un valor incompatible después de la validación.
Mutabilidad e invariancia
list es invariante. Un list[str] no puede sustituir libremente a list[object] cuando se permiten escrituras, porque la referencia más amplia podría insertar un entero. TypeGuard puede expresar relaciones que el subtipado normal rechaza, por lo que la implementación y el código posterior deben preservar la invariante del contenedor.
Cuando el predicado solo lee, aceptar Sequence[object] suele ser más seguro. Si el código modifica la colección refinada, controla que ninguna referencia externa pueda añadir valores inválidos.
Filtrar colecciones
def es_string(valor: object) -> TypeGuard[str]:
return isinstance(valor, str)
items: list[object] = ["a", 1, "b"]
textos = [item for item in items if es_string(item)]Los analizadores modernos pueden inferir list[str] en esta comprensión. El resultado con APIs de orden superior como filter() puede depender del verificador y de los stubs disponibles.
Combinar TypeGuard y Literal
from typing import Literal, TypeGuard
Modo = Literal["lectura", "escritura"]
def es_modo(valor: str) -> TypeGuard[Modo]:
return valor in {"lectura", "escritura"}Esto resulta útil para opciones de línea de comandos, variables de entorno y archivos de configuración. La guía de Literal en Python explica cómo los valores exactos mejoran overloads y APIs tipadas.
TypeGuard genérico
from typing import TypeGuard, TypeVar
T = TypeVar("T")
def sin_none(valores: list[T | None]) -> TypeGuard[list[T]]:
return all(valor is not None for valor in valores)Después de la comprobación, la lista puede tratarse como list[T]. La mutación sigue importando: otra referencia podría insertar None y romper la suposición.
Refinamiento negativo
TypeGuard está pensado principalmente para refinar la rama verdadera. La rama else puede no ser tan precisa como esperas. Python moderno también incluye TypeIs, que representa una relación de subtipo más estricta y puede refinar ambos caminos.
Elige TypeGuard cuando necesites flexibilidad, incluso para relaciones que no son subtipado ordinario. Elige TypeIs cuando el predicado identifica un subtipo o intersección real y la rama negativa debe excluirlo.
TypeGuard frente a cast
from typing import cast
usuario = cast(Usuario, datos)cast() no valida nada en runtime. Solo indica al analizador que confíe en el programador. TypeGuard conecta una comprobación real con refinamiento estático. Prefiérelo en fronteras externas y reserva cast para invariantes ya garantizadas por otro mecanismo.
TypeGuard frente a isinstance
Cuando el objetivo es una clase concreta o una tupla de clases, isinstance() ya suele refinar correctamente. TypeGuard aporta valor cuando la regla incluye estructura interna, contenido de colecciones, combinaciones de atributos o validaciones del dominio.
Probar el predicado
Escribe pruebas positivas y negativas con límites, subclases, colecciones vacías, claves ausentes y valores parecidos. Para campos enteros, recuerda que bool es subclase de int. Si debes rechazar booleanos, usa type(valor) is int en lugar de isinstance(valor, int).
Errores comunes
- Devolver True sin validación completa: el tipo prometido puede ser falso.
- Ignorar la mutación: una colección puede cambiar después de la comprobación.
- Usar TypeGuard donde basta isinstance: añade complejidad innecesaria.
- Comprobar solo parte de un TypedDict: un mapping incompleto no cumple el contrato.
- Confundir refinamiento con conversión: TypeGuard no transforma el valor.
- Esperar precisión perfecta en else: depende de la relación y del analizador.
Ejemplo completo con payload de API
from typing import TypedDict, TypeGuard
class Producto(TypedDict):
id: int
nombre: str
precio: float
def es_producto(valor: object) -> TypeGuard[Producto]:
if not isinstance(valor, dict):
return False
id_ = valor.get("id")
nombre = valor.get("nombre")
precio = valor.get("precio")
return (
type(id_) is int
and isinstance(nombre, str)
and isinstance(precio, (int, float))
and not isinstance(precio, bool)
)
def cargar(payload: object) -> Producto:
if not es_producto(payload):
raise ValueError("producto inválido")
return payloadEl cargador devuelve un Producto tipado únicamente después de validar. En sistemas reales, considera informes de error estructurados o bibliotecas de schemas cuando el consumidor necesite detalles de múltiples fallos.
Buenas prácticas
Mantén los predicados pequeños, puros y deterministas. Usa nombres que describan la condición. Haz que el parámetro refleje las entradas aceptadas. Evita efectos secundarios durante la validación. Centraliza reglas de frontera y ejecuta tanto pruebas de runtime como mypy, pyright u otro analizador en integración continua.
Conclusión
typing.TypeGuard conecta validación booleana y refinamiento estático. Es especialmente útil para datos externos, colecciones homogéneas, TypedDict, Protocol y reglas de dominio que no pueden expresarse solo con isinstance().
La documentación oficial de TypeGuard en Python define su semántica. Trata cada predicado como una promesa auditable: valida todo lo que declara el tipo, prueba entradas adversas y prefiere interfaces de solo lectura al refinar colecciones.







