domingo, 25 de enero de 2026

Aprendiendo Python de 0 a experto - Anotaciones de tipos

Introducción a Type Hinting en Python
Python // Chapter 12

Introducción a Type Hinting

Añade tipos estáticos a Python para código más robusto y mantenible

Python es un lenguaje de tipado dinámico, lo que significa que las variables no tienen tipos fijos y pueden cambiar durante la ejecución. Sin embargo, desde Python 3.5 (PEP 484), podemos añadir anotaciones de tipos a nuestro código para mejorar la legibilidad, facilitar la detección de errores y habilitar herramientas de análisis estático.

En este capítulo exploraremos: fundamentos de type hinting, el módulo typing, tipos genéricos y avanzados, verificación con mypy y mejores prácticas para escribir código Python tipado profesionalmente.

MODULE_01

¿Qué es Type Hinting?

Las anotaciones de tipo (type hints) son metadatos que indican qué tipos de datos esperan las funciones y variables. Python no las ejecuta en tiempo de ejecución, pero herramientas como mypy las utilizan para verificar la consistencia del código antes de ejecutarlo.

Beneficios

DOCUMENTACIÓN
El código se auto-documenta, haciendo explícito qué tipos espera cada función
DETECCIÓN DE ERRORES
Mypy detecta errores de tipos antes de ejecutar el código
IDE SUPPORT
Autocompletado inteligente y refactorización segura

Sintaxis Básica

Las anotaciones usan dos puntos : para variables y parámetros, y -> para el tipo de retorno:

Anatomía de una Función Tipada
parámetro: tipo
def función()
-> tipo_retorno
basic_hints.py
# Anotaciones en variables
name: str = "Alice"
age: int = 30
height: float = 1.75
is_active: bool = True

# Anotaciones en funciones
def greet(name: str) -> str:
    return f"Hello, {name}!"

# Función con múltiples parámetros
def add_numbers(a: int, b: int) -> int:
    return a + b

# Uso
result = greet("Python")
print(result)  # Hello, Python!

total = add_numbers(5, 3)
print(total)   # 8
python basic_hints.py
Hello, Python!
8
Las anotaciones de tipo son completamente opcionales. Python las ignora en tiempo de ejecución. Son metadatos para humanos y herramientas de análisis estático.
MODULE_02

El Módulo typing

El módulo typing proporciona tipos adicionales para casos más complejos que los tipos básicos de Python. Fue introducido en Python 3.5 y ha evolucionado significativamente con cada versión.

Tipos de Colecciones

collections_hints.py
from typing import List, Dict, Set, Tuple

# Lista de strings
names: List[str] = ["Alice", "Bob", "Charlie"]

# Diccionario string -> int
ages: Dict[str, int] = {"Alice": 30, "Bob": 25}

# Conjunto de enteros
unique_ids: Set[int] = {1, 2, 3, 4, 5}

# Tupla con tipos específicos por posición
coordinate: Tuple[float, float] = (10.5, 20.3)

# Tupla de longitud variable (mismo tipo)
numbers: Tuple[int, ...] = (1, 2, 3, 4, 5)

def process_users(users: List[str]) -> Dict[str, int]:
    return {user: len(user) for user in users}

result = process_users(names)
print(result)  # {'Alice': 5, 'Bob': 3, 'Charlie': 7}
Desde Python 3.9+, puedes usar los tipos nativos directamente: list[str], dict[str, int], set[int]. El módulo typing sigue siendo necesario para tipos más avanzados.

Optional y Union

Para valores que pueden ser de múltiples tipos o None:

optional_union.py
from typing import Optional, Union

# Optional[X] es equivalente a Union[X, None]
def find_user(user_id: int) -> Optional[str]:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)  # Puede retornar str o None

# Union permite múltiples tipos
def process_input(value: Union[str, int]) -> str:
    if isinstance(value, int):
        return f"Number: {value}"
    return f"String: {value}"

# Python 3.10+ sintaxis con |
def modern_process(value: str | int) -> str:
    return str(value)

# Ejemplos de uso
print(find_user(1))   # Alice
print(find_user(99))  # None
print(process_input(42))      # Number: 42
print(process_input("hello")) # String: hello

Tabla de Tipos Comunes

Tipo Descripción Ejemplo
List[T] Lista de elementos tipo T List[int]
Dict[K, V] Diccionario con claves K y valores V Dict[str, float]
Optional[T] T o None Optional[str]
Union[A, B] A o B Union[int, str]
Tuple[A, B] Tupla con tipos fijos Tuple[str, int, float]
Callable[[Args], R] Función con argumentos y retorno Callable[[int], str]
Any Cualquier tipo (escape hatch) Any
MODULE_03

Tipos Avanzados

El módulo typing ofrece tipos más sofisticados para casos de uso avanzados como funciones de orden superior, clases genéricas y restricciones de tipos.

Callable: Funciones como Tipos

callable_hints.py
from typing import Callable, List

# Tipo para funciones que toman int y retornan str
Formatter = Callable[[int], str]

def apply_formatter(
    numbers: List[int],
    formatter: Formatter
) -> List[str]:
    return [formatter(n) for n in numbers]

# Diferentes formateadores
def hex_format(n: int) -> str:
    return hex(n)

def binary_format(n: int) -> str:
    return bin(n)

nums = [10, 20, 30]
print(apply_formatter(nums, hex_format))
print(apply_formatter(nums, binary_format))
python callable_hints.py
['0xa', '0x14', '0x1e']
['0b1010', '0b10100', '0b11110']

TypeVar: Tipos Genéricos

Los TypeVar permiten crear funciones y clases genéricas que mantienen relaciones entre tipos:

TypeVar Flow
Input: T
→ función →
Output: T
typevar_hints.py
from typing import TypeVar, List, Sequence

# TypeVar para tipo genérico
T = TypeVar('T')

def first_element(items: Sequence[T]) -> T:
    """Retorna el primer elemento manteniendo el tipo."""
    return items[0]

# El tipo de retorno se infiere automáticamente
strings: List[str] = ["a", "b", "c"]
numbers: List[int] = [1, 2, 3]

first_str = first_element(strings)   # tipo: str
first_num = first_element(numbers)   # tipo: int

print(first_str, type(first_str))
print(first_num, type(first_num))

# TypeVar con restricciones
Number = TypeVar('Number', int, float)

def double(value: Number) -> Number:
    return value * 2

print(double(5))      # 10 (int)
print(double(3.14))  # 6.28 (float)

Literal: Valores Específicos

literal_hints.py
from typing import Literal

# Solo acepta estos valores específicos
Direction = Literal["north", "south", "east", "west"]

def move(direction: Direction, steps: int) -> str:
    return f"Moving {steps} steps {direction}"

print(move("north", 10))  # OK
print(move("east", 5))    # OK
# move("up", 3)  # ERROR: mypy detectará esto

# Útil para estados y configuraciones
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]

def log(message: str, level: LogLevel = "INFO") -> None:
    print(f"[{level}] {message}")
Literal es especialmente útil para APIs donde solo ciertos valores son válidos, como modos de archivo ("r", "w", "a") o estados de proceso.
MODULE_04

TypedDict y Data Classes

Python ofrece formas elegantes de definir estructuras de datos con tipos claros: TypedDict para diccionarios con estructura fija y dataclass para clases de datos.

TypedDict: Diccionarios Estructurados

typeddict_example.py
from typing import TypedDict, Optional

class User(TypedDict):
    id: int
    name: str
    email: str
    age: Optional[int]

# TypedDict con campos opcionales (Python 3.11+)
class Config(TypedDict, total=False):
    debug: bool
    timeout: int
    retries: int

def greet_user(user: User) -> str:
    return f"Hello, {user['name']}!"

# Uso correcto
alice: User = {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30
}

print(greet_user(alice))  # Hello, Alice!

Data Classes con Tipos

Las data classes combinan la definición de clases con type hints de forma elegante:

DataClass Structure
@dataclass
Atributos Tipados
__init__ Auto
__repr__ Auto
dataclass_example.py
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class Product:
    """Representa un producto en el inventario."""
    id: int
    name: str
    price: float
    quantity: int = 0
    tags: List[str] = field(default_factory=list)
    
    def total_value(self) -> float:
        return self.price * self.quantity

@dataclass
class Order:
    order_id: int
    customer: str
    products: List[Product]
    discount: Optional[float] = None
    
    def total(self) -> float:
        subtotal = sum(p.total_value() for p in self.products)
        if self.discount:
            subtotal *= (1 - self.discount)
        return subtotal

# Uso
laptop = Product(1, "Laptop", 999.99, 2, ["electronics", "computers"])
mouse = Product(2, "Mouse", 29.99, 5)

order = Order(1001, "Alice", [laptop, mouse], discount=0.1)

print(f"Product: {laptop}")
print(f"Order total: ${order.total():.2f}")
python dataclass_example.py
Product: Product(id=1, name='Laptop', price=999.99, quantity=2, tags=['electronics', 'computers'])
Order total: $1934.96
MODULE_05

Protocols: Duck Typing Estático

Los Protocols (PEP 544) permiten definir interfaces estructurales: un objeto satisface el protocolo si tiene los métodos requeridos, sin necesidad de heredar explícitamente.

Structural Subtyping
Protocol: Drawable
← satisfacen ←
Circle
Square
CustomShape
protocol_example.py
from typing import Protocol, List

# Definir un Protocol (interfaz estructural)
class Drawable(Protocol):
    def draw(self) -> str:
        ...

# Clases que satisfacen el Protocol (sin herencia)
class Circle:
    def __init__(self, radius: float):
        self.radius = radius
    
    def draw(self) -> str:
        return f"○ Circle(r={self.radius})"

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height
    
    def draw(self) -> str:
        return f"▭ Rectangle({self.width}x{self.height})"

class Triangle:
    def __init__(self, base: float):
        self.base = base
    
    def draw(self) -> str:
        return f"△ Triangle(b={self.base})"

# Función que acepta cualquier Drawable
def render_all(shapes: List[Drawable]) -> None:
    for shape in shapes:
        print(shape.draw())

# Uso - todas las clases satisfacen Drawable
shapes: List[Drawable] = [
    Circle(5.0),
    Rectangle(10.0, 20.0),
    Triangle(15.0)
]

render_all(shapes)
python protocol_example.py
○ Circle(r=5.0)
▭ Rectangle(10.0x20.0)
△ Triangle(b=15.0)
"Si camina como pato y grazna como pato, es un pato" — Los Protocols formalizan duck typing de Python con verificación estática. Las clases no necesitan heredar del Protocol explícitamente.
MODULE_06

Verificación con Mypy

mypy es el verificador de tipos estático más popular para Python. Analiza el código sin ejecutarlo y reporta inconsistencias de tipos.

Instalación y Uso

terminal
# Instalar mypy
$ pip install mypy

# Verificar un archivo
$ mypy my_script.py

# Verificar todo un directorio
$ mypy src/

# Modo estricto (más verificaciones)
$ mypy --strict my_script.py

Ejemplo de Detección de Errores

errors_demo.py
def process_name(name: str) -> str:
    return name.upper()

def get_length(items: list[int]) -> int:
    return len(items)

# Errores que mypy detectará:
result1 = process_name(123)        # Error: int != str
result2 = get_length("hello")      # Error: str != list[int]

def maybe_none() -> str | None:
    return None

value = maybe_none()
print(value.upper())  # Error: None no tiene .upper()
mypy errors_demo.py
errors_demo.py:9: error: Argument 1 to "process_name" has incompatible type "int"; expected "str"
errors_demo.py:10: error: Argument 1 to "get_length" has incompatible type "str"; expected "list[int]"
errors_demo.py:16: error: Item "None" of "str | None" has no attribute "upper"
Found 3 errors in 1 file

Configuración con pyproject.toml

pyproject.toml
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
strict_optional = true

# Ignorar librerías sin stubs
[[tool.mypy.overrides]]
module = "third_party_lib.*"
ignore_missing_imports = true
mypy solo verifica el código, no lo ejecuta. Los errores de tipo son detectados antes de ejecutar, pero no modifican el comportamiento en tiempo de ejecución.
MODULE_07

Mejores Prácticas

Aplicar type hints efectivamente requiere balance entre cobertura y practicidad. Aquí están las recomendaciones clave.

Recomendaciones

GRADUAL TYPING
Añade tipos incrementalmente. No es necesario tipar todo desde el inicio.
INTERFACES PÚBLICAS
Prioriza tipos en funciones y métodos públicos de tu API.
EVITA ANY
Usa Any solo como último recurso; elimina los beneficios del tipado.

Patrón: Type Aliases

type_aliases.py
from typing import Dict, List, Tuple, TypeAlias

# Type aliases para mejor legibilidad
UserId: TypeAlias = int
Username: TypeAlias = str
Coordinate: TypeAlias = Tuple[float, float]
UserCache: TypeAlias = Dict[UserId, Username]

# Más legible que Dict[int, Tuple[str, List[Tuple[float, float]]]]
RouteData: TypeAlias = Dict[UserId, Tuple[Username, List[Coordinate]]]

def get_user_route(data: RouteData, user_id: UserId) -> List[Coordinate]:
    user_data = data.get(user_id)
    if user_data:
        return user_data[1]
    return []

# Python 3.12+ sintaxis con 'type'
type Point = Tuple[float, float]
type Vector = List[Point]

Patrón: NewType para Tipos Distintos

newtype_example.py
from typing import NewType

# Crear tipos distintos basados en tipos existentes
UserId = NewType('UserId', int)
OrderId = NewType('OrderId', int)

def get_user(user_id: UserId) -> str:
    return f"User {user_id}"

def get_order(order_id: OrderId) -> str:
    return f"Order {order_id}"

# Uso correcto
uid = UserId(123)
oid = OrderId(456)

print(get_user(uid))   # OK
print(get_order(oid))  # OK

# mypy detectará este error:
# get_user(oid)  # Error: OrderId no es UserId
NewType crea tipos semánticamente distintos que mypy diferencia, pero en tiempo de ejecución son equivalentes al tipo base. Ideal para IDs y valores con significado específico.

Resumen de Type Hinting

01 // Tipos Básicos

str, int, float, bool con sintaxis variable: tipo y -> retorno.

02 // Módulo typing

List, Dict, Optional, Union, Callable para tipos complejos.

03 // Genéricos

TypeVar para funciones genéricas, Generic para clases parametrizadas.

04 // Protocols

Interfaces estructurales sin herencia explícita (duck typing estático).

05 // Data Classes

Combina @dataclass con type hints para clases de datos elegantes.

06 // Verificación

mypy para análisis estático. Detecta errores antes de ejecutar.

Basado en "Learn Python Programming" 4th Edition // Fabrizio Romano & Heinrich Kruger

Chapter 12: Introduction to Type Hinting