Sugerencias de tipo en Python
Aprende las sugerencias de tipo en Python: anotar variables, funciones y clases, usar el módulo typing y verificar tipos con mypy.
Las sugerencias de tipo permiten adjuntar información de tipo esperada a variables, parámetros de función y valores de retorno. Python no las aplica en tiempo de ejecución — son metadatos consumidos por editores, linters y comprobadores de tipos como mypy para detectar errores antes de ejecutar una sola línea.
Este capítulo cubre:
- Por qué son importantes las sugerencias de tipo y cuándo usarlas
- Anotar variables y funciones
- Tipos integrados y el módulo
typing(List,Dict,Optional,Union,Tuple,Any,Callable) - Sintaxis moderna (Python 3.10+)
- Anotar clases y
self - Genéricos y alias de tipo
- Análisis estático con mypy
- Errores comunes
¿Por qué usar sugerencias de tipo?
Python tiene tipado dinámico: una variable puede contener cualquier valor de cualquier tipo. Esa flexibilidad es poderosa, pero hace que los proyectos grandes sean más difíciles de navegar — no puedes saber el tipo del argumento de una función solo leyendo el lugar donde se llama.
Las sugerencias de tipo resuelven esto sin renunciar al dinamismo de Python:
- Los editores detectan errores de inmediato. VS Code, PyCharm y otros subrayan los desajustes de tipo mientras escribes.
- La refactorización se vuelve más segura. Cambia la firma de una función y el comprobador de tipos te indica cada llamada que se rompe.
- El código se documenta a sí mismo.
def greet(name: str) -> strcomunica el contrato sin necesidad de una cadena de documentación. - Las bibliotecas son más fáciles de usar. Las bibliotecas tipadas exponen autocompletado para cada atributo y método.
Las sugerencias de tipo se introdujeron en Python 3.5 mediante PEP 484. La sintaxis se ha refinado en cada versión principal desde entonces. Los ejemplos a continuación indican la versión mínima de Python en que la sintaxis estuvo disponible por primera vez.
Anotar variables
Añade dos puntos después del nombre de la variable seguidos del tipo:
name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = TrueTambién puedes declarar el tipo de una variable sin asignar un valor todavía. Esto se denomina declaración anticipada y es útil dentro de clases o a nivel de módulo:
user_id: int # declared but not yet assigned
user_id = 42Las anotaciones de tipo en variables a nivel de módulo no afectan el comportamiento en tiempo de ejecución — se almacenan en el diccionario __annotations__ del módulo, pero el intérprete las ignora por lo demás.
Anotar funciones
Coloca anotaciones en los parámetros (después de los dos puntos) y en el valor de retorno (después de -> antes de los dos puntos que cierran la firma):
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
def send_email(to: str, subject: str, body: str) -> None:
print(f"Sending '{subject}' to {to}")
result: int = add(3, 5)
message: str = greet("Alice")-> None significa que la función no tiene un valor de retorno significativo (devuelve None implícitamente). Omitir la anotación de retorno también es válido, pero -> None explícito deja clara la intención.
Parámetros con valor por defecto
Los valores por defecto van después de la anotación:
def connect(host: str, port: int = 8080, secure: bool = False) -> None:
print(f"Connecting to {host}:{port} (secure={secure})")
connect("example.com") # uses defaults
connect("example.com", 443, True)*args y **kwargs
Anota el tipo del elemento, no el tipo de la colección:
def total(*prices: float) -> float:
return sum(prices)
def create_user(**fields: str) -> dict:
return fields
print(round(total(9.99, 4.50, 12.00), 2)) # 26.49
print(create_user(name="Bob", role="admin"))*prices: float significa que cada argumento posicional es un float; en tiempo de ejecución prices sigue siendo una tuple ordinaria de floats. Del mismo modo, **fields: str significa que el valor de cada argumento nombrado es un str.
El módulo typing
Para cualquier cosa más allá de los tipos integrados básicos, importa desde el módulo typing (Python 3.5+). A partir de Python 3.9, muchos tipos de typing se fusionaron directamente en los equivalentes integrados (consulta Sintaxis moderna más adelante).
List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict
def first_names(users: List[str]) -> str:
return users[0] if users else ""
def dimensions() -> Tuple[int, int, int]:
return (1920, 1080, 32)
def unique_tags(items: List[str]) -> Set[str]:
return set(items)
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
print(first_names(["Alice", "Bob"])) # Alice
print(dimensions()) # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"])) # {'py', 'web'}
print(word_count("one two one")) # {'one': 2, 'two': 1}Optional
Optional[X] es una abreviatura de Union[X, None]. Úsalo cuando un valor puede estar ausente:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id) # returns None if not found
name = find_user(1)
if name is not None:
print(name.upper()) # ALICE
missing = find_user(99)
print(missing) # NoneUn comprobador de tipos ve Optional[str] y sabe que debes verificar si el valor es None antes de llamar a métodos de string en el resultado. Sin esa verificación, reporta un error.
Union
Union[X, Y] significa que el valor puede ser del tipo X o del tipo Y:
from typing import Union
def stringify(value: Union[int, float, str]) -> str:
return str(value)
print(stringify(42)) # 42
print(stringify(3.14)) # 3.14
print(stringify("hi")) # hiUnion es más útil cuando una función acepta genuinamente varios tipos no relacionados. Si escribes Union[str, None], usa Optional[str] en su lugar — es más idiomático.
Callable
Callable[[ArgTypes...], ReturnType] anota una función que se pasa como argumento:
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(n: int) -> int:
return n * 2
print(apply_twice(double, 3)) # 12Callable[[int], int] significa: un callable que toma un argumento int y devuelve un int. Si la lista de argumentos es compleja o desconocida, usa Callable[..., ReturnType].
Any
Any es un tipo especial que desactiva la comprobación de tipos para ese valor. Cada tipo es asignable a Any y también asignable desde Any:
from typing import Any
def log(value: Any) -> None:
print(value)
log(42)
log("hello")
log([1, 2, 3])Usa Any con moderación — es una escotilla de escape que elimina la protección que ofrecen las sugerencias de tipo. Es apropiado al interactuar con código de terceros sin tipos, o durante una migración gradual de un proyecto grande.
Sintaxis moderna (Python 3.9+, 3.10+)
Genéricos integrados (Python 3.9+)
A partir de Python 3.9, puedes usar los tipos integrados directamente como genéricos, sin importar desde typing:
# Python 3.9+
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(items: list[int]) -> int | None:
return items[0] if items else None
print(word_count("cat dog cat")) # {'cat': 2, 'dog': 1}
print(first([10, 20, 30])) # 10
print(first([])) # NoneUsa list[str] en lugar de List[str], dict[str, int] en lugar de Dict[str, int], y así sucesivamente.
Sintaxis de unión X | Y (Python 3.10+)
Python 3.10 introdujo el operador | para uniones, reemplazando Union[X, Y] y Optional[X]:
# Python 3.10+
def parse(value: str | int | None) -> str:
if value is None:
return "nothing"
return str(value)
print(parse("hello")) # hello
print(parse(42)) # 42
print(parse(None)) # nothingstr | None es equivalente a Optional[str]. Esta sintaxis es más limpia y fácil de leer.
Anotar clases
Anota los atributos de instancia dentro de __init__ y agrega anotaciones de retorno a los métodos:
class BankAccount:
owner: str # class-level annotation (no default value)
balance: float
def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0)) # True
print(account) # BankAccount(owner='Alice', balance=120.00)La anotación de self siempre se infiere — nunca escribes self: BankAccount. El tipo de retorno de __init__ es siempre None.
ClassVar
Usa ClassVar[T] (de typing) para marcar un atributo que pertenece a la clase y no a cada instancia:
from typing import ClassVar
class Config:
MAX_RETRIES: ClassVar[int] = 3
timeout: int
def __init__(self, timeout: int) -> None:
self.timeout = timeout
print(Config.MAX_RETRIES) # 3Un comprobador de tipos advierte si intentas establecer ClassVar en una instancia — está destinado a compartirse a nivel de clase.
Alias de tipo
Un alias de tipo da a un tipo largo o complejo un nombre más corto y significativo:
from typing import List, Tuple
# Simple alias
UserID = int
Filename = str
# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]
def distance(p1: Coordinates, p2: Coordinates) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
print(distance((0.0, 0.0), (3.0, 4.0))) # 5.0A partir de Python 3.12, usa la instrucción type para crear alias explícitos e inspeccionables:
# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]Genéricos con TypeVar
TypeVar te permite escribir una sola función que funcione con cualquier tipo mientras preserva las relaciones de tipo:
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
x: int = first_item([1, 2, 3]) # x is int
s: str = first_item(["a", "b"]) # s is strEl comprobador de tipos infiere a partir del argumento cuál es T y lleva esa información al tipo de retorno. Sin TypeVar, tendrías que devolver Any y perder la seguridad de tipos.
Puedes restringir TypeVar a un conjunto de tipos permitidos:
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float)
def double(n: Numeric) -> Numeric:
return n * 2
print(double(4)) # 8 (int)
print(double(2.5)) # 5.0 (float)Comprobación de tipos estática con mypy
mypy es el comprobador de tipos estático más utilizado para Python. Instálalo con pip:
pip install mypyLuego ejecútalo sobre un archivo:
mypy my_script.pyEjemplo: detectar un error con mypy
Guarda lo siguiente como demo.py:
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet(42) # passing int instead of str
print(result.upper())Al ejecutar mypy demo.py se reporta:
demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)Python en sí ejecuta el código sin problemas (los f-strings convierten cualquier tipo), pero mypy detectó el desajuste antes de que tuvieras que descubrirlo en producción.
Opciones útiles de mypy
| Opción | Efecto |
|---|---|
--strict | Activa todas las comprobaciones opcionales (recomendado para proyectos nuevos) |
--ignore-missing-imports | Suprime errores sobre stubs de terceros faltantes |
--check-untyped-defs | También comprueba tipos en funciones sin anotaciones |
--disallow-untyped-defs | Exige anotaciones en todas las definiciones de función |
Un archivo mypy.ini (o [tool.mypy] en pyproject.toml) mantiene la configuración fuera de la línea de comandos:
[mypy]
strict = true
ignore_missing_imports = trueTipado gradual
No tienes que anotar todas las funciones de una vez. Python admite el tipado gradual: el código anotado y el no anotado coexisten pacíficamente. mypy omite las funciones sin anotar por defecto (a menos que se establezca --check-untyped-defs).
Un enfoque práctico para un proyecto existente:
- Añade anotaciones al código nuevo desde el primer día.
- Anota primero las funciones más llamadas o más propensas a errores.
- Activa
--strictmódulo a módulo a medida que mejora la cobertura. - Usa
Anysolo donde una biblioteca de terceros no tenga tipos, y agrega un comentario explicando por qué.
Errores comunes
Referencias anticipadas
Si un tipo hace referencia a una clase definida más adelante en el mismo archivo, pon el nombre entre comillas para convertirlo en una cadena (una referencia anticipada):
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
head = Node(1, Node(2))
print(head.value, head.next.value) # 1 2A partir de Python 3.10+, agrega from __future__ import annotations al inicio del archivo. Esto convierte todas las anotaciones en cadenas lazy y elimina la necesidad de poner comillas manualmente.
Anotaciones en tiempo de ejecución
Por defecto, las anotaciones en Python 3.9 y anteriores se evalúan de forma ansiosa. Eso significa que una referencia anticipada sin comillas lanza un NameError:
# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...Con from __future__ import annotations (Python 3.7+), todas las anotaciones se almacenan como cadenas y solo se evalúan cuando se inspeccionan — lo que resuelve el problema de las referencias anticipadas automáticamente.
None vs Optional
Un error frecuente es anotar un tipo de retorno como str cuando la función puede devolver None. Usa siempre Optional[str] (o str | None) cuando None sea un posible retorno:
from typing import Optional
# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
if user_id == 0:
return None # type: ignore — this is the bug
# Correct
def get_name_safe(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "Alice"list vs List (compatibilidad de versiones)
Si tu código se ejecuta en Python 3.8 o anterior, debes usar from typing import List y escribir List[str]. En Python 3.9+, list[str] funciona directamente. Si necesitas compatibilidad con ambos, usa las importaciones de typing o añade from __future__ import annotations.
Referencia rápida
| Anotación | Significado |
|---|---|
x: int | La variable x es un entero |
def f(a: str) -> bool | El parámetro a es str; el valor de retorno es bool |
-> None | La función no devuelve nada significativo |
Optional[str] | str o None |
Union[int, str] | int o str |
list[int] / List[int] | Lista de enteros |
dict[str, int] / Dict[str, int] | Dict que mapea str a int |
tuple[int, str] / Tuple[int, str] | Tupla de (int, str) |
Callable[[int], str] | Función que toma int y devuelve str |
Any | Cualquier tipo (desactiva la comprobación) |
ClassVar[T] | Atributo a nivel de clase |
TypeVar("T") | Variable de tipo genérica |
Temas relacionados
- Funciones de Python — donde viven las anotaciones de tipo en parámetros y valores de retorno.
- Clases y objetos de Python — para anotar
__init__, métodos y atributos de clase. - Dataclasses de Python — las anotaciones de tipo son necesarias para declarar campos de dataclass.
- Clases abstractas de Python — las clases base abstractas funcionan de forma natural con las sugerencias de tipo.