Skip to content

Indicaciones de Tipos en Python: Una Guía Práctica de Anotaciones de Tipo

Updated on

El tipado dinámico de Python es flexible, pero crea problemas reales a escala. Las funciones aceptan tipos incorrectos en silencio y producen errores confusos lejos del bug real. El refactoring rompe código en lugares que no puedes predecir. Leer el código de otra persona significa adivinar qué tipos fluyen a través de cada variable, cada parámetro de función, cada valor de retorno. A medida que las bases de código crecen, estas suposiciones se convierten en bugs, y esos bugs se convierten en horas de depuración.

El costo se acumula en entornos de equipo. Sin información de tipos, cada llamada a función requiere leer la implementación para entender qué espera y qué devuelve. Las revisiones de código se ralentizan. Los nuevos miembros del equipo tardan más en integrarse. Las herramientas automatizadas no pueden ayudar porque no tienen información de tipos con qué trabajar.

Las indicaciones de tipos en Python resuelven esto agregando anotaciones de tipo opcionales que los IDEs, verificadores de tipos y humanos pueden verificar. Documentan tu intención directamente en el código, capturan categorías enteras de bugs antes del tiempo de ejecución y desbloquean potentes funciones de editor como autocompletado y detección de errores en línea. Esta guía cubre todo desde anotaciones básicas hasta patrones avanzados utilizados en bases de código de Python en producción.

📚

¿Qué son las Indicaciones de Tipos?

Las indicaciones de tipos son anotaciones opcionales que especifican los tipos esperados de variables, parámetros de función y valores de retorno. Fueron introducidas en PEP 484 (opens in a new tab) (Python 3.5) y han sido refinadas a través de PEPs subsecuentes incluyendo PEP 526 (anotaciones de variables), PEP 604 (sintaxis de unión) y PEP 612 (especificaciones de parámetros).

La palabra clave es opcional. Las indicaciones de tipos no afectan el comportamiento en tiempo de ejecución. Python no las hace cumplir durante la ejecución. Existen para tres audiencias: desarrolladores leyendo el código, IDEs proporcionando autocompletado y detección de errores, y verificadores de tipos estáticos como mypy que analizan código sin ejecutarlo.

# Sin indicaciones de tipos - ¿qué espera esta función?
def process_data(data, threshold, output):
    ...
 
# Con indicaciones de tipos - instantáneamente claro
def process_data(data: list[float], threshold: float, output: str) -> dict[str, float]:
    ...

La segunda versión te dice todo de un vistazo: data es una lista de floats, threshold es un float, output es un string, y la función devuelve un diccionario mapeando strings a floats. No es necesario leer la implementación o rastrear sitios de llamada.

Anotaciones de Tipos Básicas

Anotaciones de Variables

Las anotaciones de variables usan la sintaxis de dos puntos introducida en PEP 526 (Python 3.6):

# Anotaciones de variables básicas
name: str = "Alice"
age: int = 30
height: float = 5.9
is_active: bool = True
raw_data: bytes = b"hello"
 
# Anotaciones sin asignación (solo declaración)
username: str
count: int

Puedes anotar variables sin asignar un valor. Esto es útil en cuerpos de clase y bloques condicionales donde la variable se asigna más tarde.

Parámetros de Función y Tipos de Retorno

Las anotaciones de función usan dos puntos para parámetros y -> para tipos de retorno:

def greet(name: str) -> str:
    return f"Hello, {name}!"
 
def calculate_average(numbers: list[float]) -> float:
    return sum(numbers) / len(numbers)
 
def save_record(record: dict[str, str], overwrite: bool = False) -> None:
    """Las funciones que no retornan nada usan -> None."""
    ...

La anotación -> None comunica explícitamente que una función realiza una acción sin retornar un valor significativo. Esto es importante porque distingue los retornos None intencionales de las sentencias return olvidadas.

Tipos Integrados

Los tipos integrados de Python se mapean directamente a indicaciones de tipos:

TipoEjemploDescripción
intcount: int = 10Enteros
floatprice: float = 9.99Números de punto flotante
strname: str = "Bob"Cadenas de texto
boolactive: bool = TrueValores booleanos
bytesdata: bytes = b"\x00"Secuencias de bytes
Noneresult: None = NoneEl singleton None
def parse_config(path: str, encoding: str = "utf-8") -> dict[str, str]:
    config: dict[str, str] = {}
    with open(path, encoding=encoding) as f:
        for line in f:
            key, _, value = line.partition("=")
            config[key.strip()] = value.strip()
    return config

Tipos de Colecciones

Sintaxis Moderna (Python 3.9+)

A partir de Python 3.9, puedes usar tipos de colección integrados directamente como tipos genéricos:

# Listas
scores: list[int] = [95, 87, 92]
names: list[str] = ["Alice", "Bob"]
 
# Diccionarios
user_ages: dict[str, int] = {"Alice": 30, "Bob": 25}
config: dict[str, list[str]] = {"servers": ["a.com", "b.com"]}
 
# Tuplas - longitud fija con tipos específicos
point: tuple[float, float] = (3.14, 2.72)
record: tuple[str, int, bool] = ("Alice", 30, True)
 
# Tuplas de longitud variable (todo mismo tipo)
values: tuple[int, ...] = (1, 2, 3, 4, 5)
 
# Conjuntos y conjuntos inmutables
tags: set[str] = {"python", "typing"}
constants: frozenset[int] = frozenset({1, 2, 3})

Sintaxis Heredada (Python 3.5-3.8)

Antes de Python 3.9, necesitabas importar tipos genéricos del módulo typing:

from typing import List, Dict, Tuple, Set, FrozenSet
 
scores: List[int] = [95, 87, 92]
user_ages: Dict[str, int] = {"Alice": 30}
point: Tuple[float, float] = (3.14, 2.72)
tags: Set[str] = {"python", "typing"}

Tabla de Comparación de Sintaxis

TipoPython 3.9+Python 3.5-3.8
Listalist[int]typing.List[int]
Diccionariodict[str, int]typing.Dict[str, int]
Tupla (fija)tuple[str, int]typing.Tuple[str, int]
Tupla (variable)tuple[int, ...]typing.Tuple[int, ...]
Conjuntoset[str]typing.Set[str]
ConjuntoInmutablefrozenset[int]typing.FrozenSet[int]
Tipotype[MyClass]typing.Type[MyClass]

Usa la sintaxis moderna siempre que tu proyecto apunte a Python 3.9 o posterior. Es más limpia y no requiere imports.

Colecciones Anidadas

Los tipos de colección se componen naturalmente para estructuras de datos complejas:

# Matriz: lista de listas de floats
matrix: list[list[float]] = [[1.0, 2.0], [3.0, 4.0]]
 
# Mapeo de usuarios a sus listas de puntajes
gradebook: dict[str, list[int]] = {
    "Alice": [95, 87, 92],
    "Bob": [78, 85, 90],
}
 
# Configuración: diccionarios anidados
app_config: dict[str, dict[str, str | int]] = {
    "database": {"host": "localhost", "port": 5432},
    "cache": {"host": "redis.local", "port": 6379},
}

Tipos Opcionales y Uniones

Tipos Opcionales

Un valor que podría ser None se anota con Optional o la sintaxis de unión:

from typing import Optional
 
# Sintaxis pre-3.10
def find_user(user_id: int) -> Optional[str]:
    """Devuelve username o None si no se encuentra."""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)
 
# Sintaxis Python 3.10+ (preferida)
def find_user(user_id: int) -> str | None:
    """Devuelve username o None si no se encuentra."""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

Optional[str] es exactamente equivalente a str | None. La sintaxis de tubería es más legible y no requiere import.

Tipos de Unión

Cuando un valor puede ser uno de varios tipos:

from typing import Union
 
# Sintaxis pre-3.10
def format_value(value: Union[int, float, str]) -> str:
    return str(value)
 
# Sintaxis Python 3.10+ (preferida)
def format_value(value: int | float | str) -> str:
    return str(value)
 
# Patrón común: aceptar múltiples formatos de entrada
def load_data(source: str | Path) -> list[dict[str, str]]:
    path = Path(source) if isinstance(source, str) else source
    with open(path) as f:
        return json.load(f)

Comparación de Sintaxis de Unión

PatrónPre-3.10Python 3.10+
NullableOptional[str]str | None
Dos tiposUnion[int, str]int | str
MúltiplesUnion[int, float, str]int | float | str
Unión nullableOptional[Union[int, str]]int | str | None

Tipos Avanzados del módulo typing

Any

Any deshabilita la verificación de tipos para un valor específico. Úsalo con moderación como válvula de escape:

from typing import Any
 
def log_event(event: str, payload: Any) -> None:
    """Acepta cualquier tipo de payload - útil para logging genérico."""
    print(f"[{event}] {payload}")
 
# Any es compatible con todo tipo
log_event("click", {"x": 100, "y": 200})
log_event("error", 404)
log_event("message", "hello")

TypeVar y Generic

TypeVar crea variables de tipo genérico para funciones y clases que trabajan con múltiples tipos mientras preservan las relaciones de tipo:

from typing import TypeVar
 
T = TypeVar("T")
 
def first_element(items: list[T]) -> T:
    """Devuelve el primer elemento, preservando su tipo."""
    return items[0]
 
# Los verificadores de tipos inferen el tipo de retorno correcto
name = first_element(["Alice", "Bob"])     # type: str
score = first_element([95, 87, 92])        # type: int
 
# TypeVar acotado - restringir a tipos específicos
Numeric = TypeVar("Numeric", int, float)
 
def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

Creando clases genéricas:

from typing import TypeVar, Generic
 
T = TypeVar("T")
 
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
 
    def push(self, item: T) -> None:
        self._items.append(item)
 
    def pop(self) -> T:
        return self._items.pop()
 
    def peek(self) -> T:
        return self._items[-1]
 
    def is_empty(self) -> bool:
        return len(self._items) == 0
 
# Uso con tipos específicos
int_stack: Stack[int] = Stack()
int_stack.push(42)
value: int = int_stack.pop()
 
str_stack: Stack[str] = Stack()
str_stack.push("hello")

Callable

Callable anota parámetros de función y callbacks:

from typing import Callable
 
# Función que toma un callback
def apply_operation(
    values: list[float],
    operation: Callable[[float], float]
) -> list[float]:
    return [operation(v) for v in values]
 
# Uso
import math
results = apply_operation([1.0, 4.0, 9.0], math.sqrt)
# results: [1.0, 2.0, 3.0]
 
# Firmas de callable más complejas
Comparator = Callable[[str, str], int]
EventHandler = Callable[[str, dict[str, str]], None]
 
def sort_with_comparator(
    items: list[str],
    compare: Comparator
) -> list[str]:
    import functools
    return sorted(items, key=functools.cmp_to_key(compare))

Literal

Literal restringe valores a constantes específicas:

from typing import Literal
 
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
    print(f"Log level set to {level}")
 
set_log_level("INFO")      # OK
set_log_level("VERBOSE")   # Error de tipo: no es un literal válido
 
# Útil para parámetros de modo
def read_file(
    path: str,
    mode: Literal["text", "binary"] = "text"
) -> str | bytes:
    if mode == "text":
        return open(path).read()
    return open(path, "rb").read()

TypedDict

TypedDict define la forma de diccionarios con claves específicas y tipos de valor:

from typing import TypedDict, NotRequired
 
class UserProfile(TypedDict):
    name: str
    email: str
    age: int
    bio: NotRequired[str]  # Clave opcional (Python 3.11+)
 
def display_user(user: UserProfile) -> str:
    return f"{user['name']} ({user['email']}), age {user['age']}"
 
# El verificador de tipos valida la estructura
user: UserProfile = {
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30,
}
 
display_user(user)  # OK

Protocol (Subtipado Estructural)

Protocol define interfaces basadas en estructura en lugar de herencia. Si un objeto tiene los métodos requeridos, satisface el protocolo:

from typing import Protocol, runtime_checkable
 
@runtime_checkable
class Drawable(Protocol):
    def draw(self, x: int, y: int) -> None: ...
 
class Circle:
    def draw(self, x: int, y: int) -> None:
        print(f"Drawing circle at ({x}, {y})")
 
class Square:
    def draw(self, x: int, y: int) -> None:
        print(f"Drawing square at ({x}, {y})")
 
def render(shape: Drawable, x: int, y: int) -> None:
    shape.draw(x, y)
 
# Ambos funcionan sin heredar de Drawable
render(Circle(), 10, 20)
render(Square(), 30, 40)
 
# runtime_checkable habilita verificaciones isinstance
print(isinstance(Circle(), Drawable))  # True

TypeAlias

TypeAlias crea alias de tipo explícitos para tipos complejos:

from typing import TypeAlias
 
# Alias simples
UserId: TypeAlias = int
JsonDict: TypeAlias = dict[str, "JsonValue"]
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | JsonDict
 
# Alias complejos simplifican firmas
Matrix: TypeAlias = list[list[float]]
Callback: TypeAlias = Callable[[str, int], bool]
Config: TypeAlias = dict[str, str | int | list[str]]
 
def transform_matrix(m: Matrix, factor: float) -> Matrix:
    return [[cell * factor for cell in row] for row in m]
 
# Python 3.12+ usa la sentencia type
# type Vector = list[float]

Verificación de Tipos con mypy

Instalación y Ejecución de mypy

mypy es el verificador de tipos estáticos más ampliamente utilizado para Python:

# Instalar mypy
# pip install mypy
 
# Verificar un solo archivo
# mypy script.py
 
# Verificar un proyecto completo
# mypy src/
 
# Verificar con versión específica de Python
# mypy --python-version 3.10 src/

Configuración

Configura mypy en pyproject.toml o mypy.ini para ajustes de proyecto:

# Configuración en pyproject.toml
# [tool.mypy]
# python_version = "3.10"
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true
# check_untyped_defs = true
# no_implicit_optional = true
# strict_equality = true
 
# Anulaciones por módulo
# [[tool.mypy.overrides]]
# module = "third_party_lib.*"
# ignore_missing_imports = true

Banderas Comunes de mypy

BanderaEfecto
--strictHabilitar todas las verificaciones estrictas (recomendado para proyectos nuevos)
--ignore-missing-importsOmitir errores para bibliotecas de terceros sin tipos
--disallow-untyped-defsRequerir anotaciones de tipo en todas las funciones
--no-implicit-optionalNo tratar el default None como Optional
--warn-return-anyAdvertir al retornar Any desde funciones tipadas
--show-error-codesMostrar códigos de error para cada error (útil para supresión)

Solución de Errores Comunes de mypy

# Error: Incompatible return value type (got "Optional[str]", expected "str")
# Fix: Manejar el caso None
def get_name(user_id: int) -> str:
    result = lookup(user_id)  # returns str | None
    if result is None:
        raise ValueError(f"User {user_id} not found")
    return result  # mypy sabe que result es str aquí
 
# Error: Item "None" of "Optional[str]" has no attribute "upper"
# Fix: Restringir el tipo primero
def format_name(name: str | None) -> str:
    if name is not None:
        return name.upper()
    return "UNKNOWN"
 
# Error: Need type annotation for variable
# Fix: Agregar anotación explícita
items: list[str] = []  # No solo: items = []
 
# Error: Incompatible types in assignment
# Fix: Usar Union o corregir el tipo
value: int | str = 42
value = "hello"  # OK con tipo unión
 
# Suprimir errores específicos cuando sea necesario
x = some_untyped_function()  # type: ignore[no-untyped-call]

Indicaciones de Tipos en la Práctica

FastAPI y Pydantic

FastAPI usa indicaciones de tipos para impulsar validación de requests, serialización y documentación:

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class UserCreate(BaseModel):
    name: str
    email: str
    age: int
    tags: list[str] = []
 
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
 
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate) -> UserResponse:
    # FastAPI valida el body del request contra UserCreate
    # y serializa la respuesta para que coincida con UserResponse
    return UserResponse(id=1, name=user.name, email=user.email)

Pydantic usa indicaciones de tipos para validar datos automáticamente, convertir tipos y generar esquemas JSON. Las anotaciones de tipo no son solo documentación -- impulsan el comportamiento en tiempo de ejecución.

Ciencia de Datos: Tipando DataFrames

Las indicaciones de tipos son cada vez más importantes en flujos de trabajo de ciencia de datos:

import pandas as pd
from typing import Any
 
# Tipado básico de DataFrame
def clean_dataframe(df: pd.DataFrame) -> pd.DataFrame:
    return df.dropna().reset_index(drop=True)
 
# Usando pandera para validación de esquema
# pip install pandera
import pandera as pa
from pandera.typing import DataFrame, Series
 
class SalesSchema(pa.DataFrameModel):
    product: Series[str]
    quantity: Series[int] = pa.Field(ge=0)
    price: Series[float] = pa.Field(gt=0)
    date: Series[pd.Timestamp]
 
@pa.check_types
def process_sales(df: DataFrame[SalesSchema]) -> DataFrame[SalesSchema]:
    """Procesamiento de DataFrame con verificación de tipos."""
    return df[df["quantity"] > 0]

Beneficios de los IDE

Las indicaciones de tipos desbloquean potentes funciones de IDE en todos los editores principales:

CaracterísticaSin Indicaciones de TiposCon Indicaciones de Tipos
AutocompletadoSugerencias genéricasCompletados contextuales para el tipo exacto
Detección de ErroresSolo errores en tiempo de ejecuciónErrores en línea antes de la ejecución
RefactoringBúsqueda y reemplazo manualRenombrado y refactoring automatizado seguro
DocumentaciónRequiere docstrings o leer fuenteHover muestra tipos en línea
NavegaciónBúsqueda de textoSaltar a definiciones tipadas e implementaciones

Beneficios de las Indicaciones de Tipos en Diferentes Herramientas

HerramientaCómo Usa las Indicaciones de Tipos
mypyVerificación de tipos estática, captura bugs antes del runtime
Pyright/PylanceVerificación de tipos y autocompletado de VS Code
FastAPIValidación de request/response y documentación de API
PydanticValidación de datos, serialización, gestión de configuraciones
SQLAlchemy 2.0Columnas mapeadas, tipos de resultado de queries
pytestInferencia de tipos de plugins, tipado de fixtures
attrs/dataclassesGeneración automática de __init__, __repr__, __eq__

Hoja de Referencia Rápida de Indicaciones de Tipos

Aquí está una tabla de referencia rápida de las anotaciones de tipo más comunes:

AnotaciónSignificadoEjemplo
intEnterocount: int = 0
floatFlotanteprice: float = 9.99
strCadenaname: str = "Alice"
boolBooleanoactive: bool = True
bytesBytesdata: bytes = b""
NoneTipo None-> None
list[int]Lista de enterosscores: list[int] = []
dict[str, int]Diccionario mapeando str a intages: dict[str, int] = {}
tuple[str, int]Tupla de longitud fijapair: tuple[str, int]
tuple[int, ...]Tupla de longitud variablenums: tuple[int, ...]
set[str]Conjunto de stringstags: set[str] = set()
str | NoneString nullable (3.10+)name: str | None = None
Optional[str]String nullable (pre-3.10)name: Optional[str] = None
int | strInt o string (3.10+)value: int | str
Union[int, str]Int o string (pre-3.10)value: Union[int, str]
AnyCualquier tipo (válvula de escape)data: Any
Callable[[int], str]Tipo de funciónfn: Callable[[int], str]
Literal["a", "b"]Solo valores específicosmode: Literal["r", "w"]
TypeVar("T")Variable de tipo genéricoT = TypeVar("T")
ClassVar[int]Variable a nivel de clasecount: ClassVar[int] = 0
Final[str]No puede reasignarseNAME: Final = "app"
TypeAliasAlias de tipo explícitoUserId: TypeAlias = int

Errores Comunes

ErrorProblemaSolución
def f(x: list)Falta tipo de elementodef f(x: list[int])
items = []El tipo no puede inferirseitems: list[str] = []
def f(x: int = None)El default es None pero el tipo es intdef f(x: int | None = None)
from typing import List (3.9+)Import innecesarioUsar list[int] directamente
def f(x: dict)Faltan tipos de clave/valordef f(x: dict[str, int])
isinstance(x, list[int])No se pueden usar genéricos con isinstanceisinstance(x, list)
def f() -> TrueUsando un valor, no un tipodef f() -> bool
Anotar selfRedundante, mypy lo infiereOmitir anotación de self
x: str = 42Anotación incorrectaHacer coincidir anotación con tipo real
Sobreusar AnyDerrota el propósito del tipadoUsar tipos específicos o TypeVar
# Error: default mutable con indicación de tipo
def bad_append(item: str, items: list[str] = []) -> list[str]:
    items.append(item)  # ¡Default mutable compartido!
    return items
 
# Fix: usar None como default
def good_append(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items

Ejemplo Práctico: Una Canalización de Datos Tipada

Aquí hay un ejemplo completo mostrando cómo las indicaciones de tipos trabajan juntas en un escenario real de procesamiento de datos. Este patrón es común en flujos de trabajo de ciencia de datos y analytics:

from typing import TypedDict, Callable, TypeAlias
from pathlib import Path
import csv
 
# Definir formas de datos con TypedDict
class RawRecord(TypedDict):
    name: str
    value: str
    category: str
 
class ProcessedRecord(TypedDict):
    name: str
    value: float
    category: str
    normalized: float
 
# Type alias para funciones de transformación
Transform: TypeAlias = Callable[[list[ProcessedRecord]], list[ProcessedRecord]]
 
def load_csv(path: Path) -> list[RawRecord]:
    """Cargar datos CSV con salida tipada."""
    records: list[RawRecord] = []
    with open(path) as f:
        reader = csv.DictReader(f)
        for row in reader:
            records.append(RawRecord(
                name=row["name"],
                value=row["value"],
                category=row["category"],
            ))
    return records
 
def parse_records(raw: list[RawRecord]) -> list[ProcessedRecord]:
    """Convertir registros raw de strings a registros tipados."""
    max_value = max(float(r["value"]) for r in raw) or 1.0
    return [
        ProcessedRecord(
            name=r["name"],
            value=float(r["value"]),
            category=r["category"],
            normalized=float(r["value"]) / max_value,
        )
        for r in raw
    ]
 
def filter_by_category(
    records: list[ProcessedRecord],
    category: str
) -> list[ProcessedRecord]:
    """Filtrar registros, entrada y salida completamente tipadas."""
    return [r for r in records if r["category"] == category]
 
def apply_transforms(
    records: list[ProcessedRecord],
    transforms: list[Transform]
) -> list[ProcessedRecord]:
    """Aplicar una cadena de funciones de transformación tipadas."""
    result = records
    for transform in transforms:
        result = transform(result)
    return result
 
# Uso
raw = load_csv(Path("data.csv"))
processed = parse_records(raw)
filtered = filter_by_category(processed, "electronics")

Cada función en esta canalización tiene tipos de entrada y salida claros. Un verificador de tipos puede verificar que la salida de una función coincida con la entrada de la siguiente. Si alguien cambia la estructura de ProcessedRecord, mypy marca cada lugar que necesita actualización.

Visualización de Datos Tipados con PyGWalker

Cuando trabajas con DataFrames tipados en flujos de trabajo de ciencia de datos, PyGWalker (opens in a new tab) convierte tus DataFrames de pandas en una interfaz de visualización interactiva similar a Tableau. Funciona bien junto con canalizaciones verificadas por tipos porque los datos estructurados que produces con tipado apropiado se alimentan directamente en gráficos y dashboards explorables:

import pandas as pd
import pygwalker as pyg
 
# Tu canalización tipada produce datos limpios y estructurados
data: list[ProcessedRecord] = parse_records(raw)
df = pd.DataFrame(data)
 
# PyGWalker lo renderiza como una visualización interactiva
walker = pyg.walk(df)

Para entornos de notebooks interactivos, RunCell (opens in a new tab) proporciona una experiencia Jupyter potenciada por IA donde el código verificado por tipos y la exploración visual de datos trabajan juntos sin problemas.

Preguntas Frecuentes

¿Las indicaciones de tipos afectan el rendimiento de Python?

No. El runtime de Python ignora las indicaciones de tipos por completo. Se almacenan como metadatos en funciones y variables pero nunca se evalúan durante la ejecución normal. Hay una sobrecarga de memoria despreciable por almacenar las anotaciones, pero ningún impacto en la velocidad de ejecución. Frameworks como Pydantic y FastAPI sí leen las anotaciones al inicio para construir lógica de validación, pero esto es comportamiento del framework, no una característica del lenguaje Python.

¿Son obligatorias las indicaciones de tipos en Python?

No. Las indicaciones de tipos son completamente opcionales. Python sigue siendo un lenguaje de tipado dinámico, y el código se ejecuta idénticamente con o sin anotaciones. Sin embargo, las indicaciones de tipos son fuertemente recomendadas para cualquier proyecto con más de un desarrollador o cualquier base de código que necesite mantenimiento a largo plazo. Proyectos principales de Python como FastAPI, SQLAlchemy y Django dependen cada vez más de las indicaciones de tipos.

¿Cuál es la diferencia entre indicaciones de tipos y verificación de tipos?

Las indicaciones de tipos son las anotaciones que escribes en tu código, como x: int o -> str. La verificación de tipos es el proceso de verificar que tu código sea consistente con esas anotaciones. La verificación de tipos es realizada por herramientas externas como mypy, Pyright o Pylance -- no por Python mismo. Puedes tener indicaciones de tipos sin ejecutar un verificador de tipos, y las indicaciones aún proporcionan valor a través del autocompletado del IDE y la documentación.

¿Debería usar typing.List o list para indicaciones de tipos?

Usa list[int] en minúsculas si tu proyecto apunta a Python 3.9 o posterior. typing.List[int] es la sintaxis heredada requerida para Python 3.5-3.8. La sintaxis en minúsculas es más limpia, no requiere imports y es el enfoque recomendado de ahora en adelante. Lo mismo aplica para dict vs typing.Dict, tuple vs typing.Tuple y set vs typing.Set.

¿Cuál es el mejor verificador de tipos para Python?

mypy es el verificador de tipos más estable y ampliamente utilizado para Python. Pyright (usado por la extensión Pylance de VS Code) es más rápido y captura algunos errores que mypy no detecta. Ambos están activamente mantenidos. Para la mayoría de proyectos, usa el que se integre mejor con tu editor. mypy es el estándar para pipelines de CI. Pyright proporciona la mejor experiencia en tiempo real en VS Code. Puedes ejecutar ambos en un proyecto sin conflictos.

Conclusión

Las indicaciones de tipos en Python cierran la brecha entre la flexibilidad dinámica de Python y las garantías de seguridad de los lenguajes de tipado estático. Capturan bugs antes del tiempo de ejecución, hacen que el código se auto-documente y desbloquean potentes funciones de IDE que aceleran el desarrollo.

Comienza con lo básico: anota parámetros de función, tipos de retorno y variables complejas. Usa list[int], dict[str, str] y str | None para tipos cotidianos. Ejecuta mypy en tu pipeline de CI para capturar errores de tipo automáticamente. A medida que crece tu confianza, adopta patrones avanzados como TypedDict, Protocol y Generic para modelar tipos de dominio complejos.

La inversión se paga rápidamente. Una sola anotación de tipo que previene un bug en producción justifica horas de esfuerzo de tipado. Los equipos reportan integración más rápida, refactoring más seguro y menos sorpresas en tiempo de ejecución después de adoptar indicaciones de tipos. Con frameworks como FastAPI y Pydantic construyendo todo su diseño alrededor de las anotaciones de tipo, Python tipado no es una práctica de nicho -- es la dirección hacia la que se mueve el ecosistema.

📚