Anotaciones de tipo en Python
En esta lección
Python es un lenguaje de tipado dinámico: una variable puede guardar un número y, más tarde, un texto, y nadie se queja hasta que algo falla al ejecutar. Desde Python 3.5 puedes añadir anotaciones de tipo (type hints) que dicen qué tipo de dato esperas en cada sitio. No cambian cómo se ejecuta el programa, pero hacen el código más fácil de leer, permiten que el editor te avise de errores mientras escribes y que herramientas como mypy encuentren fallos antes de ejecutar. En esta lección aprenderás a anotar tu código con la sintaxis moderna de Python 3.12.
Anotar variables
Se escribe el nombre, dos puntos y el tipo:
nombre: str = "Lucía"
edad: int = 19
altura: float = 1.68
matriculada: bool = True
Para los tipos básicos casi nunca hace falta: Python y el editor ya deducen el tipo a partir del valor (edad = 19 es claramente un int). Las anotaciones de variables son útiles sobre todo cuando la variable empieza vacía y el tipo no es evidente:
notas: list[float] = []
precios: dict[str, float] = {}
Anotar funciones
Donde las anotaciones más ayudan es en las funciones: cada parámetro lleva su tipo y, tras una flecha ->, el tipo de lo que devuelve.
def precio_final(precio: float, descuento: float = 0) -> float:
return round(precio * (1 - descuento / 100), 2)
def saludar(nombre: str) -> None:
print(f"Hola, {nombre}")
print(precio_final(80, 25)) # 60.0
saludar("Iker") # Hola, Iker
- Un parámetro con valor por defecto se anota igual:
descuento: float = 0. -> Noneindica que la función no devuelve nada.- Donde se pide
float, se acepta también unint(80), porque todo entero vale como número decimal.
Solo con leer la primera línea sabes cómo usar la función, sin tener que estudiar su código. Además, el editor (VS Code, PyCharm) usará esa información para autocompletar y para avisarte si le pasas algo raro.
Colecciones
Entre corchetes se indica el tipo de lo que contienen:
| Anotación | Significado |
|---|---|
list[int] | Lista de enteros |
set[str] | Conjunto de cadenas |
dict[str, float] | Diccionario con claves str y valores float |
tuple[str, int] | Tupla de exactamente dos elementos: un str y un int |
tuple[int, ...] | Tupla de enteros de cualquier longitud |
list[dict[str, int]] | Lista de diccionarios |
def media(notas: list[float]) -> float:
return sum(notas) / len(notas)
def nota_maxima(alumnos: dict[str, float]) -> tuple[str, float]:
nombre = max(alumnos, key=alumnos.get)
return nombre, alumnos[nombre]
print(media([7, 8.5, 6])) # 7.166666666666667
print(nota_maxima({"Ana": 8.5, "Luis": 9.1})) # ('Luis', 9.1)
Nota: en código antiguo verás
from typing import List, Dict, Tupley anotaciones comoList[int]. Desde Python 3.9 se usan directamentelist,dictytupleen minúscula.
Python no comprueba los tipos
Esto es lo más importante de la lección: las anotaciones no se comprueban al ejecutar. Son información para las personas y las herramientas, no para el intérprete:
def doble(n: int) -> int:
return n * 2
print(doble("ja")) # jaja
Aunque doble pide un int, Python ejecuta la función con una cadena sin protestar. Las anotaciones se guardan en el atributo __annotations__, pero nadie las mira a menos que tú uses una herramienta para ello. Si necesitas validar los datos que llegan al programa (lo que escribe el usuario, lo que viene de un archivo), sigue haciéndolo con if y raise, como en excepciones.
mypy: el comprobador de tipos
mypy es un programa que lee tu código sin ejecutarlo y comprueba que los tipos encajan. Se instala con pip, mejor dentro de un entorno virtual:
pip install mypy
mypy tienda.py
Supón que tienda.py contiene esto:
def precio_final(precio: float, descuento: float = 0) -> float:
return round(precio * (1 - descuento / 100), 2)
total = precio_final("80")
mensaje: str = precio_final(50)
mypy responde:
tienda.py:4: error: Argument 1 to "precio_final" has incompatible type "str"; expected "float" [arg-type]
tienda.py:5: error: Incompatible types in assignment (expression has type "float", variable has type "str") [assignment]
Found 2 errors in 1 file (checked 1 source file)
Te dice el archivo, la línea y qué no encaja. Son errores que, sin anotaciones, solo descubrirías al ejecutar ese trozo de código, quizá semanas después y en el ordenador de un cliente.
Consejo: en VS Code, la extensión de Python (con Pylance) hace este tipo de comprobaciones mientras escribes y subraya los errores en rojo. mypy es útil para comprobar el proyecto entero de una vez o hacerlo automáticamente antes de subir cambios.
Valores que pueden faltar: | None
Muchas funciones devuelven un resultado o nada. Se anota con la barra vertical |, que significa “o”:
def buscar_telefono(agenda: dict[str, str], nombre: str) -> str | None:
return agenda.get(nombre)
agenda = {"Lucía": "600111222"}
telefono = buscar_telefono(agenda, "Pablo")
if telefono is None:
print("No está en la agenda")
else:
print(telefono.replace("600", "+34 600"))
# No está en la agenda
Gracias a la anotación, mypy sabe que telefono puede ser None y te avisa si intentas llamar a telefono.replace(...) sin comprobarlo antes, uno de los errores más habituales (AttributeError: 'NoneType' object has no attribute...). Dentro del else, en cambio, ya sabe que es un str: a esto se le llama estrechar el tipo.
La barra sirve para cualquier combinación: int | str acepta un entero o una cadena.
Nota: en código anterior a Python 3.10 verás
Optional[str](equivale astr | None) yUnion[int, str](equivale aint | str), importados detyping.
Tipos más precisos con typing
El módulo typing añade herramientas para describir datos con más detalle.
Literal: solo unos valores concretos
from typing import Literal
def pedir_cafe(tamano: Literal["pequeño", "mediano", "grande"]) -> float:
precios = {"pequeño": 1.2, "mediano": 1.5, "grande": 1.9}
return precios[tamano]
print(pedir_cafe("grande")) # 1.9
Si escribes pedir_cafe("gigante"), mypy lo marca como error, y el editor te sugiere los tres valores válidos al escribir.
TypedDict: diccionarios con forma fija
Cuando un diccionario tiene siempre las mismas claves (por ejemplo, datos que vienen de un JSON), TypedDict describe qué claves tiene y de qué tipo es cada una:
from typing import TypedDict
class Ruta(TypedDict):
nombre: str
km: float
desnivel: int
ruta: Ruta = {"nombre": "Pico Jano", "km": 11.5, "desnivel": 780}
print(ruta["km"] * 2) # 23.0
En ejecución sigue siendo un diccionario normal, pero mypy detectará una clave mal escrita (ruta["kms"]) o un valor de otro tipo.
Callable: funciones como parámetro
Cuando un parámetro es una función, se anota con Callable[[tipos de los parámetros], tipo devuelto]:
from collections.abc import Callable
def aplicar_a_todos(precios: list[float], operacion: Callable[[float], float]) -> list[float]:
return [operacion(p) for p in precios]
def con_iva(p: float) -> float:
return round(p * 1.21, 2)
print(aplicar_a_todos([10, 20], con_iva)) # [12.1, 24.2]
Callable[[float], float] se lee: “una función que recibe un float y devuelve un float”. Es lo que viste en funciones avanzadas al pasar funciones como argumentos.
Any: cualquier cosa
Any desactiva la comprobación para ese valor: mypy acepta cualquier operación sobre él. Úsalo solo como último recurso, por ejemplo con datos de una librería que no tiene anotaciones.
Alias de tipo
Si una anotación es larga o se repite mucho, dale un nombre con type (Python 3.12):
type Coordenada = tuple[float, float]
type Inventario = dict[str, list[int]]
def distancia_aprox(a: Coordenada, b: Coordenada) -> float:
return round(((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5, 3)
print(distancia_aprox((43.46, -3.80), (43.35, -4.05))) # 0.273
Un buen nombre (Coordenada) explica qué representan los datos mejor que tuple[float, float]. En versiones anteriores a la 3.12 se escribía simplemente Coordenada = tuple[float, float].
Genéricos
A veces una función trabaja con listas de cualquier tipo, pero quieres decir que el tipo que devuelve es el mismo que el de los elementos. Para eso se usa una variable de tipo, que se declara entre corchetes después del nombre de la función:
def primero[T](elementos: list[T]) -> T:
return elementos[0]
nombre = primero(["Ana", "Luis"]) # mypy sabe que es str
numero = primero([3, 1, 2]) # mypy sabe que es int
print(nombre.upper(), numero + 1) # ANA 4
T es como un hueco: si le pasas una list[str], T vale str y la función devuelve un str. Se llaman genéricos y los usan mucho las librerías. En código anterior a 3.12 verás la forma antigua: T = TypeVar("T") antes de la función.
Dataclasses con tipos
En clases viste que @dataclass necesita anotaciones para saber qué atributos tiene la clase. Con tipos precisos, mypy comprueba también cómo creas los objetos:
from dataclasses import dataclass, field
@dataclass
class Socio:
nombre: str
cuota: float
actividades: list[str] = field(default_factory=list)
email: str | None = None
def apuntar(self, actividad: str) -> None:
self.actividades.append(actividad)
s = Socio("Marta", 29.9)
s.apuntar("escalada")
print(s)
# Socio(nombre='Marta', cuota=29.9, actividades=['escalada'], email=None)
Si escribes Socio("Marta", "29.9") o s.apuntar(3), mypy lo marca. En las clases normales se anotan igual los parámetros de __init__ y de los métodos; self no se anota.
¿Merece la pena anotar?
No es obligatorio, y en un script de 20 líneas puede ser excesivo. Pero en cuanto un programa crece o trabajáis varias personas, las anotaciones se pagan solas: documentan el código, el editor autocompleta mejor y muchos errores aparecen al escribir en lugar de al ejecutar. Una estrategia razonable:
- Anota siempre los parámetros y el retorno de las funciones.
- Anota las variables solo cuando el tipo no sea evidente (colecciones vacías,
Noneal principio). - Pasa mypy de vez en cuando, o deja que tu editor lo haga por ti.
Errores frecuentes
- Creer que Python comprueba los tipos: no lo hace. Sin mypy o el editor, una anotación incorrecta pasa desapercibida.
- Olvidar
| None: si la función puede devolverNone(por ejemplo, condict.geto unreturnen unif), dilo en la anotación. - Usar
listsin el tipo interior:lista secas no dice nada del contenido; escribelist[str]. - Confundir
tuple[int]contuple[int, ...]: la primera es una tupla de un elemento. - Abusar de
Any: anula la comprobación y oculta errores. - Escribir
-> Nonecuando la función sí devuelve algo: mypy avisará de que el valor devuelto no encaja.
Resumen
| Elemento | Ejemplo |
|---|---|
| Variable | edad: int = 19 |
| Función | def f(precio: float, n: int = 1) -> float: |
| No devuelve nada | -> None |
| Colecciones | list[int], dict[str, float], tuple[str, int] |
| Puede faltar | str | None (antes Optional[str]) |
| Varios tipos | int | str (antes Union[int, str]) |
| Valores fijos | Literal["S", "M", "L"] |
| Diccionario con forma | class Ruta(TypedDict): ... |
| Función como parámetro | Callable[[float], float] |
| Alias | type Coordenada = tuple[float, float] |
| Genérico | def primero[T](xs: list[T]) -> T: |
| Comprobar | pip install mypy y mypy archivo.py |
En la siguiente lección aprenderás a trabajar con días, horas y duraciones con el módulo datetime.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.