Ir al contenido principal

Type hints: anotaciones de tipo

Introducción

Las anotaciones de tipo o type hints son una característica de Python que permite especificar el tipo de datos esperado para variables, parámetros de funciones y valores de retorno. Introducidas en Python 3.5 y mejoradas en versiones posteriores, estas anotaciones no afectan al comportamiento del código en tiempo de ejecución, pero proporcionan información valiosa para herramientas de análisis estático, entornos de desarrollo y desarrolladores que trabajen con el código. En este artículo, exploraremos qué son las anotaciones de tipo, cómo utilizarlas correctamente y qué beneficios aportan a nuestros proyectos en Python.

Ventajas de las anotaciones de tipo

Las anotaciones de tipo ofrecen varios beneficios importantes:

  • Detección temprana de errores: Las herramientas de análisis estático pueden encontrar errores de tipo antes de ejecutar el código.
  • Mejor documentación: El código se vuelve más autodocumentado.
  • Mejor autocompletado: Los IDEs pueden proporcionar sugerencias más precisas.
  • Refactorizaciones más seguras: Facilitan la modificación y mantenimiento del código.
  • Desarrollo más rápido: Reduce la necesidad de documentación adicional sobre tipos esperados.

Sintaxis básica de anotaciones

Anotaciones en variables

# Anotaciones de variables básicas
nombre: str = "Ana"
edad: int = 25
altura: float = 1.75
es_estudiante: bool = True

# Variables sin asignación inicial
contador: int  # Solo declara el tipo

Anotaciones en funciones

def saludar(nombre: str) -> str:
    """Función que devuelve un saludo personalizado."""
    return f"¡Hola, {nombre}!"

def calcular_precio(cantidad: int, precio_unitario: float) -> float:
    """Calcula el precio total de una compra."""
    return cantidad * precio_unitario

def procesar_datos(datos: list) -> None:
    """Procesa datos sin devolver un valor."""
    for dato in datos:
        print(dato)

Tipos compuestos

Para estructuras de datos más complejas, necesitamos importar tipos del módulo typing:

from typing import List, Dict, Tuple, Set, Optional, Union

# Lista tipada
numeros: List[int] = [1, 2, 3, 4, 5]

# Diccionario tipado
personas: Dict[str, int] = {"Ana": 25, "Carlos": 30, "Elena": 28}

# Tupla tipada
coordenadas: Tuple[float, float] = (40.416775, -3.703790)

# Conjunto tipado
frutas: Set[str] = {"manzana", "plátano", "naranja"}

# Tipos opcionales (puede ser None)
resultado: Optional[int] = None

# Unión de tipos (puede ser uno u otro)
identificador: Union[str, int] = "A12345"  # Podría ser también un entero

A partir de Python 3.9, se pueden usar los tipos nativos con la sintaxis de corchetes:

# En Python 3.9+
numeros: list[int] = [1, 2, 3, 4, 5]
personas: dict[str, int] = {"Ana": 25, "Carlos": 30}

Tipos especiales

Any

El tipo Any representa cualquier tipo y es útil cuando no queremos restringir el tipo:

from typing import Any

def procesar_cualquier_dato(dato: Any) -> Any:
    # Esta función acepta y devuelve cualquier tipo
    return dato

TypeVar y genéricos

Para funciones que preservan el tipo de entrada:

from typing import TypeVar, List, Sequence

T = TypeVar('T')  # Define un tipo genérico T

def primer_elemento(secuencia: Sequence[T]) -> T:
    """Devuelve el primer elemento de una secuencia."""
    return secuencia[0]

# Se puede usar con diferentes tipos
primer_numero = primer_elemento([1, 2, 3])  # Devuelve int
primer_texto = primer_elemento(["a", "b", "c"])  # Devuelve str

Callable

Para funciones que reciben o devuelven otras funciones:

from typing import Callable

# Una función que recibe otra función
def aplicar_dos_veces(func: Callable[[int], int], valor: int) -> int:
    """Aplica una función dos veces a un valor."""
    return func(func(valor))

def duplicar(x: int) -> int:
    return x * 2

resultado = aplicar_dos_veces(duplicar, 3)  # 12

Anotaciones para clases

Las anotaciones también funcionan con clases y métodos:

class Persona:
    def __init__(self, nombre: str, edad: int) -> None:
        self.nombre: str = nombre
        self.edad: int = edad
    
    def saludar(self) -> str:
        return f"Hola, me llamo {self.nombre}"
    
    def es_mayor_que(self, otra_persona: 'Persona') -> bool:
        """Compara la edad con otra persona."""
        return self.edad > otra_persona.edad

Nota el uso de comillas en 'Persona' para referencias adelantadas (forward references).

Anotaciones para estructuras más complejas

Literales de tipo

Para restringir valores concretos:

from typing import Literal

def configurar_nivel(nivel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) -> None:
    print(f"Nivel configurado a {nivel}")

# Correcto
configurar_nivel("DEBUG")

# Error (detectado por herramientas de análisis)
configurar_nivel("TRACE")  # "TRACE" no está en la lista de valores permitidos

TypedDict

Para diccionarios con estructura definida:

from typing import TypedDict

class DatosPelicula(TypedDict):
    titulo: str
    anio: int
    director: str
    puntuacion: float

# Uso
star_wars: DatosPelicula = {
    "titulo": "Star Wars: Episodio IV",
    "anio": 1977,
    "director": "George Lucas",
    "puntuacion": 8.6
}

Union Type y Pipe

A partir de Python 3.10, se puede usar el operador | para uniones:

# Antes de Python 3.10
from typing import Union
identificador: Union[str, int] = "A12345"

# En Python 3.10+
identificador: str | int = "A12345"

Verificación de tipos

Para verificar los tipos, se pueden usar herramientas externas como:

Mypy

Mypy es la herramienta más común para análisis estático de tipos:

# Instalación
pip install mypy

# Uso
mypy mi_archivo.py

Ejemplo de error detectado por Mypy:

def sumar(a: int, b: int) -> int:
    return a + b

resultado = sumar("5", 10)  # Mypy detectará este error

Pyright/Pylance

Pyright (usado por VS Code como Pylance) es otra herramienta popular:

# Instalación
pip install pyright

# Uso
pyright mi_archivo.py

Compatibilidad con versiones anteriores

Para usar anotaciones en código que debe ser compatible con versiones anteriores de Python:

from __future__ import annotations  # Para Python 3.7+

# Esto permite usar sintaxis de anotaciones más reciente
# en versiones más antiguas
class Arbol:
    def agregar_hijo(self, hijo: 'Arbol') -> None:
        # El código...
        pass

Documentación con anotaciones

Las anotaciones complementan muy bien docstrings:

def calcular_estadisticas(numeros: list[float]) -> dict[str, float]:
    """
    Calcula estadísticas básicas de una lista de números.
    
    Args:
        numeros: Lista de valores numéricos a analizar.
    
    Returns:
        Diccionario con las estadísticas (media, mediana, desviación).
    
    Raises:
        ValueError: Si la lista está vacía.
    """
    if not numeros:
        raise ValueError("La lista no puede estar vacía")
    
    n = len(numeros)
    media = sum(numeros) / n
    
    # Más cálculos...
    
    return {
        "media": media,
        "mediana": sorted(numeros)[n // 2],
        "desviacion": sum((x - media) ** 2 for x in numeros) / n ** 0.5
    }

Mejores prácticas con anotaciones de tipo

  1. Ser consistente: Si decides usar anotaciones, úsalas en todo el código.
  2. Empezar por las interfaces públicas: Anotar primero las funciones y clases que se expondrán.
  3. No sobreannotar: No es necesario anotar variables obvias dentro de funciones.
  4. Usar herramientas de verificación: Integrar mypy u otra herramienta en tu flujo de trabajo.
  5. Gradualidad: Python permite tipado gradual, aprovecha esta flexibilidad.
  6. Mantén actualizada la documentación: Las anotaciones complementan, no reemplazan, la documentación.

Casos comunes de uso

APIs y bibliotecas públicas

def obtener_datos_api(url: str, parametros: Optional[dict[str, str]] = None) -> dict:
    """Obtiene datos JSON de una API."""
    # Implementación...

Funciones complejas

from typing import List, Dict, Any, Optional

def procesar_resultados(
    datos: List[Dict[str, Any]],
    filtrar_por: Optional[str] = None,
    ordenar_por: str = "fecha",
    descendente: bool = True
) -> List[Dict[str, Any]]:
    """Procesa y filtra resultados de consulta."""
    # Implementación...

Resumen

Las anotaciones de tipo en Python proporcionan una capa adicional de documentación y seguridad a nuestro código, sin afectar su comportamiento en tiempo de ejecución. Permiten detectar errores de forma temprana, mejoran la experiencia de desarrollo y hacen que el código sea más mantenible. Aunque su uso es opcional, se han convertido en una práctica muy recomendada, especialmente en proyectos de tamaño medio y grande. En el próximo artículo exploraremos patrones de diseño comunes en Python, que nos ayudarán a estructurar nuestro código de manera más efectiva cuando abordemos problemas complejos.