Type hints y mypy en Python: tipado estático opcional
- ¿Por qué anotar tipos en un lenguaje dinámico?
- Sintaxis básica: variables, funciones y retornos
- Optional, Union, Any y los tipos compuestos
- Colecciones tipadas: list, dict, tuple y set
- TypeVar y Generic: tipos polimórficos
- Protocol: duck typing con verificación estática
- dataclass y TypedDict: estructuras de datos tipadas
- mypy: verificación estática en la práctica
- Preguntas frecuentes
Python es dinámico desde su nacimiento: las variables no tienen tipo, las funciones aceptan cualquier argumento, y el intérprete resuelve todo en tiempo de ejecución. Durante décadas eso fue una virtud — menos código, más flexibilidad, prototipado rápido. Pero conforme los proyectos crecen a decenas de módulos y docenas de desarrolladores, esa libertad se convierte en coste: ¿qué devuelve exactamente esta función? ¿Este parámetro puede ser None? ¿Qué tiene este diccionario? Los type hints son la respuesta de Python: tipado opcional que no rompe la dinamicidad pero añade una capa de documentación ejecutable y verificable.
En esta lección aprendemos a anotar código Python con el módulo typing, los tipos compuestos modernos (Python 3.10+), TypeVar, Protocol, dataclass tipadas y TypedDict. Luego configuramos mypy para verificar estáticamente el código antes de ejecutarlo. El resultado: código que se documenta solo, IDEs que autocompletan con precisión quirúrgica, y errores de tipos detectados en el editor en lugar del servidor de producción.
¿Por qué anotar tipos en un lenguaje dinámico?
Los type hints (PEP 484, Python 3.5+) son anotaciones que describen los tipos esperados de variables, parámetros y valores de retorno. El intérprete las ignora completamente en tiempo de ejecución — son para herramientas de análisis estático, IDEs y documentación. No cambian el comportamiento del programa, pero sí la calidad del ecosistema que lo rodea.
# ── Sin anotaciones: el misterio ──────────────────────────────
def procesar(datos, config, modo):
# ¿Qué es datos? ¿una lista? ¿un dict? ¿un DataFrame?
# ¿config puede ser None? ¿modo es un string o un enum?
# Solo la documentación (si existe) o leer la implementación responde
resultado = datos.transform(config, modo)
return resultado # ¿qué devuelve?
# ── Con anotaciones: el contrato explícito ────────────────────
from typing import Optional
from dataclasses import dataclass
@dataclass
class Config:
umbral: float
modo: str
def procesar(
datos: list[dict],
config: Config,
modo: Optional[str] = None
) -> dict[str, float]:
# Ahora el IDE sabe exactamente qué métodos/atributos tienen datos y config
# mypy puede verificar que los tipos sean correctos antes de ejecutar
# el lector del código entiende el contrato sin leer la implementación
...
# ── Los tres beneficios concretos ────────────────────────────
# 1. DOCUMENTACIÓN VIVA: las anotaciones no se quedan obsoletas como
# los comentarios — si cambias la firma, el type checker avisa
# 2. AUTOCOMPLETADO PRECISO: el IDE conoce el tipo de cada variable
# y ofrece solo los métodos/atributos válidos
# 3. DETECCIÓN TEMPRANA DE ERRORES:
def calcular_descuento(precio: float, porcentaje: float) -> float:
return precio * (1 - porcentaje / 100)
# mypy detecta ANTES de ejecutar:
calcular_descuento("19.99", 10) # ← error: str no es float
calcular_descuento(19.99, "10") # ← error: str no es float
resultado: int = calcular_descuento(19.99, 10) # ← error: float no es int
# ── Las anotaciones en tiempo de ejecución: ignoradas ─────────
def saludar(nombre: str) -> str:
return f"Hola, {nombre}!"
# Python NO comprueba en ejecución:
print(saludar(42)) # funciona: "Hola, 42!" — sin error de runtime
print(saludar(None)) # funciona: "Hola, None!" — sin error de runtime
# Las anotaciones son accesibles como metadatos:
import inspect
print(inspect.get_annotations(saludar)) # {'nombre': , 'return': }
Sintaxis básica: variables, funciones y retornos
La sintaxis de anotación usa : tipo para variables y parámetros, y -> tipo para el retorno de funciones. Desde Python 3.9 los tipos built-in soportan suscripción directa (list[int] en lugar de List[int] del módulo typing). Desde Python 3.10 se puede usar | para unions.
# ── Variables ──────────────────────────────────────────────────
nombre: str = "Ana"
edad: int = 32
precio: float = 19.99
activo: bool = True
datos: bytes = b"\x00\xff"
# Anotación sin asignación (declare forward)
resultado: int # declara que existirá, sin valor aún
# ── Funciones: parámetros y retorno ───────────────────────────
def saludar(nombre: str, veces: int = 1) -> str:
return (f"Hola, {nombre}! " * veces).strip()
def suma(a: int, b: int) -> int:
return a + b
def sin_retorno(mensaje: str) -> None:
print(mensaje) # None = la función no devuelve valor útil
# ── Colecciones básicas (Python 3.9+) ─────────────────────────
def procesar_nombres(nombres: list[str]) -> list[str]:
return [n.strip().title() for n in nombres]
def contar_palabras(texto: str) -> dict[str, int]:
palabras = texto.lower().split()
return {p: palabras.count(p) for p in set(palabras)}
def primera_ultima(items: tuple[int, ...]) -> tuple[int, int]:
return items[0], items[-1]
def unicos(valores: set[float]) -> set[float]:
return {round(v, 2) for v in valores}
# ── Tuplas heterogéneas (longitud fija, tipos distintos) ───────
Coordenada = tuple[float, float]
Rango = tuple[int, int] # inicio, fin
RGB = tuple[int, int, int] # rojo, verde, azul
def distancia(p1: Coordenada, p2: Coordenada) -> float:
import math
return math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)
# ── Callable: funciones como tipos ────────────────────────────
from typing import Callable
def aplicar(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)
def aplicar_todos(
fns: list[Callable[[float], float]],
valor: float
) -> list[float]:
return [fn(valor) for fn in fns]
resultado = aplicar(lambda x, y: x + y, 3, 4) # 7
# ── Python 3.12: type alias explícito ─────────────────────────
type Punto = tuple[float, float] # nueva sintaxis 3.12
type Tabla = dict[str, list[int]]
# Antes (compatible con versiones anteriores):
from typing import TypeAlias
Punto: TypeAlias = tuple[float, float]
Optional, Union, Any y los tipos compuestos
Optional[X] es el tipo más usado en la práctica — representa un valor que puede ser X o None. Union[X, Y] (o X | Y en Python 3.10+) representa valores que pueden ser de más de un tipo. Any es la válvula de escape: desactiva la verificación de tipos para ese valor.
from typing import Optional, Union, Any, Literal, Final, TypeGuard
# ── Optional: el tipo más frecuente ───────────────────────────
# Optional[X] ≡ Union[X, None] ≡ X | None (Python 3.10+)
def buscar_usuario(id: int) -> Optional[dict]:
# puede devolver un dict o None si no existe
...
def crear_conexion(
host: str,
puerto: int = 5432,
password: str | None = None # sintaxis moderna
) -> object:
...
# ── Narrowing: verificar el tipo antes de usar ────────────────
def mostrar_nombre(usuario: dict | None) -> str:
if usuario is None:
return "Anónimo"
# mypy sabe que aquí usuario es dict (no None)
return usuario.get("nombre", "Sin nombre")
# ── Union: múltiples tipos posibles ──────────────────────────
def normalizar(valor: int | float | str) -> float:
if isinstance(valor, str):
return float(valor.replace(",", "."))
return float(valor)
# mypy infiere el tipo correcto dentro de cada rama isinstance
def procesar(item: list[int] | dict[str, int] | None) -> int:
if item is None:
return 0
if isinstance(item, list):
return sum(item) # mypy sabe: item es list[int]
return sum(item.values()) # mypy sabe: item es dict[str, int]
# ── Literal: valores exactos permitidos ──────────────────────
from typing import Literal
Modo = Literal["lectura", "escritura", "append"]
Nivel = Literal[1, 2, 3]
def abrir_archivo(ruta: str, modo: Modo = "lectura") -> None:
...
abrir_archivo("datos.csv", "lectura") # OK
# abrir_archivo("datos.csv", "borrar") # error mypy: no es un Literal válido
# ── Final: constantes inmutables ─────────────────────────────
from typing import Final
MAX_REINTENTOS: Final = 3
URL_BASE: Final[str] = "https://api.ejemplo.com"
# MAX_REINTENTOS = 5 ← mypy error: no se puede reasignar Final
# ── Any: la válvula de escape ─────────────────────────────────
from typing import Any
def log(valor: Any) -> None:
"""Acepta cualquier cosa — usamos Any con intención."""
print(repr(valor))
# Any desactiva la verificación para ese valor
# Úsalo solo cuando sea inevitable (código muy dinámico, interop)
# Evita usar Any como solución rápida a errores de tipo
# ── TypeGuard: type narrowing personalizado ──────────────────
from typing import TypeGuard
def es_lista_de_str(val: list[Any]) -> TypeGuard[list[str]]:
"""Si devuelve True, mypy sabe que val es list[str]."""
return all(isinstance(x, str) for x in val)
def procesar_strings(datos: list[Any]) -> list[str]:
if es_lista_de_str(datos):
return [s.upper() for s in datos] # mypy: datos es list[str]
raise TypeError("Se esperaba list[str]")
Colecciones tipadas: list, dict, tuple y set
Python 3.9+ permite usar directamente los tipos built-in con suscripción. Para código compatible con Python 3.8 o anterior, es necesario importar los equivalentes de typing (List, Dict, Tuple, Set). La sintaxis moderna es mucho más limpia.
from typing import Sequence, Mapping, MutableMapping, Iterable, Iterator
# ── Preferir tipos abstractos en parámetros ───────────────────
# Ser permisivo en lo que aceptas, estricto en lo que produces
# MAL: demasiado concreto en el parámetro
def sumar_lista(nums: list[int]) -> int:
return sum(nums)
# sumar_lista((1, 2, 3)) ← falla mypy: tuple no es list
# sumar_lista(range(10)) ← falla mypy: range no es list
# BIEN: aceptar cualquier secuencia iterable
def sumar_secuencia(nums: Sequence[int]) -> int:
return sum(nums)
# sumar_secuencia([1, 2, 3]) ← OK
# sumar_secuencia((1, 2, 3)) ← OK
# sumar_secuencia(range(10)) ← OK
def imprimir_todos(items: Iterable[str]) -> None:
for item in items:
print(item)
# ── Mapping vs dict ───────────────────────────────────────────
# Mapping: solo lectura (get, keys, values, items)
def contar_total(precios: Mapping[str, float]) -> float:
return sum(precios.values())
# MutableMapping: lectura y escritura
def normalizar_precios(precios: MutableMapping[str, float]) -> None:
for k in precios:
precios[k] = round(precios[k], 2)
# ── Iterator y Generator ──────────────────────────────────────
from typing import Generator
def contar_hasta(n: int) -> Iterator[int]:
for i in range(n):
yield i
# Generator[YieldType, SendType, ReturnType]
def fibonacci() -> Generator[int, None, None]:
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# ── dict con estructura conocida ──────────────────────────────
# Para dicts con claves conocidas: TypedDict (ver sección siguiente)
# Para dicts homogéneos (todas las claves str, todos los valores int):
Frecuencias = dict[str, int]
Coordenadas = dict[str, float] # {"x": 1.0, "y": 2.5}
# ── Tipos anidados ────────────────────────────────────────────
Matriz = list[list[float]]
GrupoAlum = dict[str, list[str]] # tutor → [alumnos]
ResultadoDB = list[dict[str, int | str | float | None]]
def transponer(matriz: Matriz) -> Matriz:
if not matriz:
return []
return [[fila[i] for fila in matriz] for i in range(len(matriz[0]))]
# ── Compatibilidad Python 3.8 (typing imports) ────────────────
# Python 3.9+: list[int], dict[str, int], tuple[int, ...]
# Python 3.8-: from typing import List, Dict, Tuple, Set, FrozenSet
# from __future__ import annotations → activa nueva sintaxis en 3.7+
TypeVar y Generic: tipos polimórficos
Un TypeVar es una variable de tipo que permite escribir funciones y clases que funcionan con múltiples tipos manteniendo la relación entre ellos. Es el mecanismo de generics de Python: la función recibe T y devuelve T — si el input es int, el output también es int.
from typing import TypeVar, Generic
T = TypeVar("T") # cualquier tipo
N = TypeVar("N", int, float) # solo int o float (TypeVar acotado)
# ── TypeVar en funciones: preservar el tipo ───────────────────
def primero(lista: list[T]) -> T:
return lista[0]
n: int = primero([1, 2, 3]) # mypy: devuelve int
s: str = primero(["a", "b"]) # mypy: devuelve str
# i: int = primero(["a", "b"]) # ← error mypy: str no es int
def segundo_elemento(
par: tuple[T, T]
) -> T:
return par[1]
x: float = segundo_elemento((1.5, 2.7)) # devuelve float
y: str = segundo_elemento(("a", "b")) # devuelve str
# ── TypeVar con cota (bound): restringir a una jerarquía ──────
from typing import Sized
S = TypeVar("S", bound=Sized) # S debe tener __len__
def mas_largo(a: S, b: S) -> S:
return a if len(a) >= len(b) else b
resultado_str = mas_largo("hola", "mundo") # str
resultado_list = mas_largo([1, 2], [1, 2, 3]) # list[int]
# ── Clase Generic: contenedores tipados ──────────────────────
class Pila(Generic[T]):
"""Pila LIFO genérica."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Pila vacía")
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
def __bool__(self) -> bool:
return bool(self._items)
# mypy infiere el tipo según el uso:
pila_int: Pila[int] = Pila()
pila_int.push(42)
n: int = pila_int.pop() # int, no Any
pila_str: Pila[str] = Pila()
pila_str.push("hola")
# pila_str.push(42) ← error mypy: int no es str
# ── Python 3.12: sintaxis simplificada de generics ────────────
# SIN TypeVar declarado explícitamente:
def primero_nuevo[T](lista: list[T]) -> T: # Python 3.12+
return lista[0]
class Cola[T]: # Python 3.12+
def __init__(self) -> None:
self._items: list[T] = []
def encolar(self, item: T) -> None:
self._items.append(item)
def desencolar(self) -> T:
return self._items.pop(0)
# ── ParamSpec: TypeVar para parámetros de funciones ──────────
from typing import ParamSpec, Concatenate
import functools
P = ParamSpec("P")
def registrar(func: Callable[P, T]) -> Callable[P, T]:
"""Decorador que preserva la firma completa de la función."""
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Llamando a {func.__name__}")
return func(*args, **kwargs)
return wrapper
@registrar
def calcular(x: int, y: float, modo: str = "suma") -> float:
...
# mypy sabe que calcular tiene exactamente la misma firma que el original
Protocol: duck typing con verificación estática
Un Protocol define una interfaz estructural: cualquier clase que implemente los métodos especificados es compatible, sin necesidad de herencia explícita. Es la versión estáticamente verificable del duck typing — si camina como un pato y grazna como un pato, mypy lo acepta como pato.
from typing import Protocol, runtime_checkable
# ── Protocol básico ───────────────────────────────────────────
class Serializable(Protocol):
"""Cualquier objeto que pueda convertirse a dict."""
def to_dict(self) -> dict[str, object]: ...
def to_json(self) -> str: ...
# Estas clases NO heredan de Serializable:
class Usuario:
def __init__(self, nombre: str, email: str):
self.nombre = nombre
self.email = email
def to_dict(self) -> dict[str, object]:
return {"nombre": self.nombre, "email": self.email}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict())
class Producto:
def __init__(self, sku: str, precio: float):
self.sku = sku
self.precio = precio
def to_dict(self) -> dict[str, object]:
return {"sku": self.sku, "precio": self.precio}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict())
# mypy acepta Usuario y Producto como Serializable porque implementan la interfaz
def guardar(obj: Serializable, ruta: str) -> None:
with open(ruta, "w") as f:
f.write(obj.to_json())
guardar(Usuario("Ana", "ana@ejemplo.com"), "usuario.json") # OK
guardar(Producto("SKU-001", 29.99), "producto.json") # OK
# ── @runtime_checkable: usar Protocol con isinstance ─────────
@runtime_checkable
class Dibujable(Protocol):
def dibujar(self) -> None: ...
def area(self) -> float: ...
class Circulo:
def __init__(self, radio: float):
self.radio = radio
def dibujar(self) -> None:
print(f"○ Círculo r={self.radio}")
def area(self) -> float:
import math
return math.pi * self.radio ** 2
c = Circulo(5)
print(isinstance(c, Dibujable)) # True (comprueba métodos en runtime)
# ── Protocol con métodos de clase y propiedades ───────────────
class ConNombre(Protocol):
@property
def nombre(self) -> str: ...
@classmethod
def crear(cls, datos: dict) -> "ConNombre": ...
# ── Protocolo de comparación: Comparable ──────────────────────
from typing import Protocol
class Comparable(Protocol):
def __lt__(self, other: object) -> bool: ...
def __le__(self, other: object) -> bool: ...
C = TypeVar("C", bound=Comparable)
def minimo(items: list[C]) -> C:
"""Funciona con cualquier tipo comparable: int, float, str, fecha..."""
return min(items)
print(minimo([3, 1, 4, 1, 5])) # 1
print(minimo(["banana", "apple"])) # 'apple'
# ── Protocol vs ABC: cuándo usar cada uno ─────────────────────
# Protocol → cuando NO controlas las clases (librerías externas)
# cuando quieres duck typing verificado
# cuando no necesitas super() ni herencia de implementación
# ABC → cuando CONTROLAS todas las clases y quieres herencia explícita
# cuando usas métodos abstractos con implementación parcial
# cuando necesitas @abstractmethod + implementación default
dataclass y TypedDict: estructuras de datos tipadas
Las dataclass generan automáticamente __init__, __repr__, __eq__ y más a partir de las anotaciones de campo. TypedDict añade tipos a los diccionarios con estructura conocida sin cambiar su tipo en runtime. Juntos eliminan el 90% del boilerplate en la definición de estructuras de datos.
from dataclasses import dataclass, field, KW_ONLY
from typing import TypedDict, NotRequired
# ── dataclass básica ──────────────────────────────────────────
@dataclass
class Punto:
x: float
y: float
def distancia_al_origen(self) -> float:
import math
return math.sqrt(self.x**2 + self.y**2)
p = Punto(3.0, 4.0)
print(p) # Punto(x=3.0, y=4.0) — __repr__ automático
print(p.distancia_al_origen()) # 5.0
print(Punto(1.0, 2.0) == Punto(1.0, 2.0)) # True — __eq__ automático
# ── dataclass con valores por defecto ─────────────────────────
from datetime import datetime
@dataclass
class Empleado:
nombre: str
email: str
departamento: str = "Sin asignar"
activo: bool = True
fecha_alta: datetime = field(default_factory=datetime.now)
habilidades: list[str] = field(default_factory=list)
# NO: lista_default: list = [] ← error: mutable default no permitido
_id: int = field(default=0, repr=False) # sin repr
e = Empleado("Ana García", "ana@empresa.com", "Ingeniería")
print(e.habilidades) # [] — lista nueva para cada instancia
# ── dataclass frozen: inmutable ───────────────────────────────
@dataclass(frozen=True)
class Coordenada:
latitud: float
longitud: float
# e.latitud = 1.0 ← FrozenInstanceError en runtime
# Útil como clave de diccionario (hashable)
cache: dict[Coordenada, str] = {}
cache[Coordenada(40.4168, -3.7038)] = "Madrid"
# ── dataclass con __post_init__: validación ───────────────────
@dataclass
class Rango:
inicio: int
fin: int
def __post_init__(self) -> None:
if self.inicio > self.fin:
raise ValueError(f"inicio ({self.inicio}) > fin ({self.fin})")
if self.fin - self.inicio > 10000:
raise ValueError("Rango demasiado amplio")
def __contains__(self, valor: int) -> bool:
return self.inicio <= valor <= self.fin
r = Rango(1, 100)
print(5 in r) # True
print(200 in r) # False
# ── TypedDict: diccionarios con tipos conocidos ───────────────
class UsuarioAPI(TypedDict):
id: int
nombre: str
email: str
admin: bool
# Campos opcionales con NotRequired (Python 3.11+)
class ConfigApp(TypedDict):
debug: bool
host: str
puerto: int
timeout: NotRequired[float] # opcional
log_dir: NotRequired[str] # opcional
# mypy verifica el acceso a claves
def procesar_usuario(usuario: UsuarioAPI) -> str:
return f"{usuario['nombre']} <{usuario['email']}>"
u: UsuarioAPI = {"id": 1, "nombre": "Ana", "email": "ana@ej.com", "admin": False}
print(procesar_usuario(u))
# Diferencia con dataclass: TypedDict sigue siendo un dict en runtime
print(type(u)) #
print(u["id"]) # 1
# u["inexistente"] ← mypy error; en runtime KeyError
# ── Combinar TypedDict y dataclass ───────────────────────────
@dataclass
class Pedido:
usuario: UsuarioAPI
productos: list[dict[str, float]] # [{"sku": "...", "precio": ...}]
total: float = field(init=False)
def __post_init__(self) -> None:
self.total = sum(p["precio"] for p in self.productos)
mypy: verificación estática en la práctica
mypy es el type checker oficial de Python, mantenido por el equipo de Dropbox que propuso el sistema de tipos en PEP 484. Analiza el código sin ejecutarlo y detecta errores de tipos, argumentos incorrectos, atributos inexistentes y retornos incompatibles antes de que lleguen a producción.
# ── Instalación y uso básico ──────────────────────────────────
# pip install mypy
# Verificar un archivo:
# mypy mi_script.py
# Verificar un módulo completo:
# mypy mi_paquete/
# Modo estricto (recomendado para proyectos nuevos):
# mypy --strict mi_script.py
# ── Errores comunes que detecta mypy ─────────────────────────
# 1. Tipo incorrecto en argumento
def cuadrado(n: int) -> int:
return n * n
cuadrado("5") # error: Argument 1 has incompatible type "str"; expected "int"
# 2. Atributo inexistente
class Punto:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Punto(1.0, 2.0)
p.z # error: "Punto" has no attribute "z"
# 3. Retorno incompatible
def dividir(a: float, b: float) -> float:
if b == 0:
return "Error: división por cero" # error: str no es float
return a / b
# 4. None no verificado
def obtener_nombre(d: dict) -> str:
return d.get("nombre") # error: Optional[str] no es str
# Correcto:
def obtener_nombre_ok(d: dict) -> str:
return d.get("nombre") or "Desconocido"
# ── Configuración: mypy.ini / pyproject.toml ─────────────────
# [mypy]
# python_version = 3.11
# warn_return_any = True
# warn_unused_configs = True
# disallow_untyped_defs = True
# ignore_missing_imports = True
#
# [mypy-pandas.*]
# ignore_missing_imports = True
# [mypy-requests.*]
# ignore_missing_imports = True
# pyproject.toml equivalente:
# [tool.mypy]
# python_version = "3.11"
# strict = true
# ignore_missing_imports = true
# ── Silencios selectivos: # type: ignore ─────────────────────
import unknown_lib # type: ignore[import] # librería sin stubs
resultado: str = funcion_any() # type: ignore[assignment]
# ── cast: decirle a mypy el tipo exacto ──────────────────────
from typing import cast
datos_raw = obtener_datos_db() # devuelve Any
datos: list[dict[str, str]] = cast(list[dict[str, str]], datos_raw)
# cast no hace nada en runtime — es solo para mypy
# ── reveal_type: depurar inferencia de mypy ──────────────────
# Solo funciona con mypy, no en runtime normal
x = [1, 2, 3]
reveal_type(x) # mypy revela: list[int]
# ── Estrategia de adopción gradual ───────────────────────────
# 1. Empezar con: mypy --ignore-missing-imports archivo.py
# 2. Anotar solo funciones públicas / interfaces principales
# 3. Activar: --disallow-untyped-defs para exigir anotaciones
# 4. Activar: --warn-return-any, --strict-optional
# 5. Modo estricto: --strict (equivale a activar todo)
# 6. Añadir a CI/CD: mypy --strict src/ como paso antes de tests
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Type hints y mypy en Python: tipado estático opcional
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Type hints y mypy en Python: tipado estático opcional? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!