Los type hints son anotaciones que describes en la firma de una función para indicar qué tipos esperas recibir y qué tipo devuelves. La sintaxis es def f(x: int, y: str) -> bool: — los dos puntos después del parámetro para entradas, la flecha para la salida. Simple de leer, pero hay una trampa mental que atrapa a casi todos la primera vez: Python no hace nada con esas anotaciones en tiempo de ejecución.
El intérprete las evalúa (o en algunos contextos, las guarda como strings sin evaluarlas), las almacena en el atributo __annotations__ de la función, y sigue adelante. Si pasas un float donde anotaste int, Python no lanza ninguna excepción. No hay coerción, no hay validación implícita. Las anotaciones existen exclusivamente para herramientas externas: type checkers como mypy o pyright, IDEs, linters, y sistemas de documentación.
Esto es un diseño deliberado. Python tomó la decisión de mantener los type hints como información opcional y sin coste en runtime, en lugar de convertirlos en un sistema de tipos estricto que rompería la filosofía de duck typing del lenguaje. El PEP 484 (2014) lo dejó claro: las anotaciones son una capa de documentación verificable, no un contrato ejecutado.
¿Cuándo valen la pena? En código que otros van a consumir — APIs públicas de módulos, funciones en equipos grandes, librerías — los type hints son documentación que un type checker puede verificar de forma continua. En un script exploratorio de 40 líneas o un notebook de análisis de datos que vas a tirar mañana, añaden ruido sin beneficio real.
El riesgo de hacerlo mal no es un crash en producción — es falsa confianza. Si anotas una función y asumes que está “tipada y segura”, pero nadie ejecuta mypy en el proyecto, tienes el costo de escritura sin el beneficio de la verificación.
from __future__ import annotations # (1)
from collections.abc import Sequence
from typing import Optional
def find_first(
items: Sequence[str],
target: str,
case_sensitive: bool = True,
) -> int | None: # (2)
"""Return the index of the first occurrence of target, or None."""
for i, item in enumerate(items):
candidate = item if case_sensitive else item.lower()
needle = target if case_sensitive else target.lower()
if candidate == needle:
return i
return None
def summarize(
values: Sequence[float],
label: Optional[str] = None, # (3)
) -> dict[str, float]:
if not values:
raise ValueError("Cannot summarize an empty sequence")
result: dict[str, float] = {
"min": min(values),
"max": max(values),
"mean": sum(values) / len(values),
}
if label is not None:
# Store label length as a float to keep the return type homogeneous
result["label_len"] = float(len(label))
return result
# __annotations__ es un dict ordinario — accesible en runtime
print(find_first.__annotations__)
# {'items': 'Sequence[str]', 'target': 'str',
# 'case_sensitive': 'bool', 'return': 'int | None'}
# Nada impide esto en runtime — mypy sí lo detectaría
print(find_first([1, 2, 3], "a")) # type: ignore[arg-type]
from __future__ import annotationsactiva la evaluación diferida (PEP 563): todas las anotaciones se guardan como strings en lugar de evaluarse al definir la función. Esto evita errores de referencias circulares y mejora el tiempo de importación en módulos con muchas anotaciones.int | Nonees la sintaxis union disponible desde Python 3.10. En versiones anteriores necesitasOptional[int]detyping, que es exactamente equivalente.Optional[str]aparece aquí intencionadamente junto aint | Nonepara que veas ambas formas. En proyectos que usan Python ≥ 3.10, la sintaxisX | Nonees preferible por ser más directa.
Errores que debes conocer
Error: Asumir que el type hint valida el valor en runtime y omitir validación explícita.
# ❌ Wrong
def process_age(age: int) -> str:
return f"Tienes {age * 12} meses"
process_age("treinta") # Falla con TypeError en el *, no antes
# ✅ Right
def process_age(age: int) -> str:
if not isinstance(age, int):
raise TypeError(f"age debe ser int, recibido {type(age).__name__!r}")
return f"Tienes {age * 12} meses"
El hint documenta la intención, pero si la función es parte de una API pública que recibe datos de fuentes externas, la validación explícita con isinstance sigue siendo necesaria.
Error: Usar list[str] en lugar de Sequence[str] cuando solo necesitas iterar, acoplando innecesariamente a una implementación concreta.
# ❌ Wrong — rechaza tuplas, generadores y cualquier otra secuencia
def first_upper(items: list[str]) -> str:
return items[0].upper()
# ✅ Right — acepta cualquier cosa con __getitem__ y __len__
from collections.abc import Sequence
def first_upper(items: Sequence[str]) -> str:
return items[0].upper()
Sequence de collections.abc es el tipo correcto cuando solo lees la secuencia; reserva list para cuando realmente necesitas mutarla.
Error: Anotar variables locales triviales hasta el punto de hacer el código más difícil de leer que sin anotaciones.
# ❌ Wrong — ruido sin valor
def double(x: int) -> int:
result: int = x * 2
temp: int = result + 0
return temp
# ✅ Right — anota la firma, deja que la inferencia haga su trabajo
def double(x: int) -> int:
return x * 2
Los type checkers infieren tipos locales perfectamente bien; las anotaciones de variables locales solo aportan valor cuando la inferencia no puede resolver el tipo o cuando la asignación inicial es None y el tipo real llega después.
N° 58