Los type hints en Python son anotaciones que le dicen a las herramientas estáticas —mypy, pyright, tu IDE— qué tipo espera una función o qué tipo contiene una variable. En tiempo de ejecución Python las ignora casi por completo; su valor está en la verificación antes de ejecutar el código.
La sintaxis vive en dos lugares: en las anotaciones de variables (x: int = 5) y en las firmas de funciones (def f(x: str) -> bool). El interpretador guarda esas anotaciones en __annotations__, pero no hace ninguna comprobación. Quien comprueba es el type checker.
El diseño intencionado de que sean ignoradas en runtime tiene una consecuencia importante: puedes mentir. x: int = "hola" es Python válido que ejecuta sin error. Eso hace que la disciplina de mantener las anotaciones correctas sea responsabilidad tuya y de tu toolchain.
¿Cuándo merece la pena anotar? Siempre que el código vaya a ser leído por más de una persona, o por ti mismo en tres meses. Las anotaciones son documentación ejecutable que mypy puede refutar.
¿Qué rompe si lo haces mal? Dos cosas: anotar con tipos demasiado amplios (Any en todas partes) inutiliza el checker; anotar con tipos incorrectos genera falsos negativos que te dan falsa seguridad.
Vamos a ver la gama completa de sintaxis, desde lo más simple hasta los tipos de colecciones de collections.abc.
from __future__ import annotations # (1)
from collections.abc import Iterable, Iterator, Mapping, Sequence
from typing import Any, Union
# ── Tipos básicos ────────────────────────────────────────────────────────
def greet(name: str) -> str:
return f"Hola, {name}"
def is_adult(age: int) -> bool:
return age >= 18
def log_value(value: float) -> None: # None como retorno: la función no retorna valor útil
print(f"[LOG] {value}")
# ── Colecciones genéricas (sintaxis moderna, Python 3.9+) ────────────────
def sum_scores(scores: list[int]) -> int:
return sum(scores)
def word_count(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def first_and_last(items: tuple[str, int]) -> str: # tupla de longitud y tipos fijos
label, value = items
return f"{label}: {value}"
# ── Opcionalidad ─────────────────────────────────────────────────────────
def find_user(user_id: int) -> str | None: # Python 3.10+: X | None reemplaza Optional[X]
users = {1: "alice", 2: "bob"}
return users.get(user_id) # .get devuelve None si no existe
def parse_number(raw: str) -> int | float | None: # Union de más de dos tipos (3.10+)
try:
return int(raw)
except ValueError:
try:
return float(raw)
except ValueError:
return None
# Equivalente pre-3.10 usando Union explícito (útil si necesitas compatibilidad)
def parse_number_compat(raw: str) -> Union[int, float, None]:
return parse_number(raw)
# ── Any como escape hatch ────────────────────────────────────────────────
def deserialize(payload: Any) -> dict[str, Any]: # (2)
import json
return json.loads(payload)
# ── Tipos abstractos de collections.abc ─────────────────────────────────
def total_length(items: Sequence[str]) -> int: # acepta list, tuple, str, range…
return sum(len(s) for s in items)
def print_keys(mapping: Mapping[str, int]) -> None: # acepta dict, OrderedDict, etc.
for key in mapping:
print(key)
def first_even(numbers: Iterable[int]) -> int | None: # acepta cualquier iterable
for n in numbers:
if n % 2 == 0:
return n
return None
def count_down(start: int) -> Iterator[int]: # retorna algo que produce valores uno a uno
while start >= 0:
yield start
start -= 1
(1)
from __future__ import annotationshace que todas las anotaciones del módulo se evalúen de forma perezosa (como strings). En Python < 3.10 esto permite usar la sintaxisX | Yylist[int]sin error en runtime, incluso en versiones anteriores al soporte nativo.(2)
Anyes compatible con cualquier tipo en ambas direcciones: puedes pasar unAnydonde se esperaint, y puedes pasar unintdonde se esperaAny. El checker simplemente deja de verificar.
La distinción entre list[int] y Sequence[int] es la decisión más importante que encontrarás en práctica. Cuando anotas un parámetro como list[int], le estás diciendo al caller “dame exactamente una lista”. Si lo que necesitas es recorrer los elementos, eso es demasiado restrictivo: una tupla, un generador, el resultado de un map —todos son válidos y ninguno es list. Sequence[int] acepta cualquier cosa indexable y con len; Iterable[int] acepta incluso generadores que solo puedes recorrer una vez.
La regla práctica: en parámetros, pide la interfaz más estrecha que necesites; en valores de retorno, sé más concreto para que el caller sepa qué puede hacer con el resultado.
str | None es exactamente Optional[str] de typing; la sintaxis de barra es más legible y no requiere imports. Para código que necesita correr en Python 3.9, añade from __future__ import annotations y puedes usarla sin romper nada. Para 3.8 o anterior, quédate con Optional.
None como tipo de retorno merece una nota: no es lo mismo que omitir la anotación. Sin anotación, mypy asume -> None por defecto en algunos contextos, pero en otros infiere Unknown. Ser explícito evita ambigüedad y documenta la intención.
Any tiene un uso legítimo: boundaries con código dinámico, resultados de json.loads, respuestas de APIs externas sin esquema tipado. El problema surge cuando se filtra hacia adentro del código propio. Una variable Any “contamina” las expresiones en las que participa, volviendo todo Any corriente abajo. Contén Any lo antes posible usando cast o validando el tipo con una guarda.
Errores que debes conocer
Error: usar list sin parámetro de tipo cuando el checker necesita inferir el contenido, o mezclar la sintaxis antigua List[int] de typing con la moderna en el mismo archivo, generando confusión innecesaria.
# ❌ Wrong
from typing import List, Dict
def process(items: List[int]) -> Dict[str, int]:
...
# ✅ Right
def process(items: list[int]) -> dict[str, int]:
...
List y Dict de typing están deprecados desde 3.9. En código nuevo usa directamente los builtins.
Error: anotar un parámetro como list[int] cuando la función solo itera, rechazando sin querer generadores y tuplas válidos.
# ❌ Wrong — obliga al caller a materializar un list aunque no sea necesario
def average(numbers: list[int]) -> float:
return sum(numbers) / len(numbers)
# ✅ Right — pero con cuidado: Sequence permite len(), Iterable no
from collections.abc import Sequence
def average(numbers: Sequence[int]) -> float:
return sum(numbers) / len(numbers)
Sequence mantiene acceso a len (necesario aquí para el divisor) y acepta listas, tuplas y cualquier secuencia indexable.
Error: usar X | Y sin from __future__ import annotations en Python 3.9, lo que lanza TypeError en runtime porque la evaluación de anotaciones todavía es eager en esa versión.
# ❌ Wrong en Python 3.9 sin el import
def find(key: str) -> int | None:
...
# ✅ Right
from __future__ import annotations
def find(key: str) -> int | None:
...
El import hace que las anotaciones se almacenen como strings y no se evalúen al definir la función, eliminando el error.
N° 126