Portada del artículo: Tipado en Python: qué son los type hints, cuándo usarlos y por qué importan con la IA

Tipado en Python: qué son los type hints, cuándo usarlos y por qué importan con la IA

Python no te pide declarar qué tipo de dato guarda una variable. Tampoco qué devuelve una función. Por eso escribes rápido y pruebas ideas en minutos.

El problema llega cuando el proyecto crece, o cuando tu código recibe datos de una API o de un modelo de IA. Una función que a veces devuelve un diccionario y a veces None termina fallando en el peor momento, con el clásico:

AttributeError: 'NoneType' object has no attribute 'get'

Ahí entra el tipado en Python, también llamado type hints o anotaciones de tipo. En esta guía vas a ver qué hacen los dos puntos (:) y la flecha (->), cuándo vale la pena tipar y cuándo es trabajo de más, cómo se escribe desde Python 3.10 y por qué los tipos se volvieron clave para trabajar con modelos de IA y agentes.

Para quién es: para quien programa en Python (o está aprendiendo) y quiere código que falle menos. También para quien conecta Python con un modelo de IA y necesita respuestas con un formato fijo.


En corto

  • Los dos puntos (:) dicen qué tipo de dato espera una variable o un parámetro: nombre: str.
  • La flecha (->) dice qué devuelve una función: def calcular() -> float:.
  • Python no revisa los tipos cuando corre el programa. Los usan tu editor, verificadores como mypy o pyright y librerías como Pydantic.
  • Tipa: funciones que se reutilizan, APIs, datos que entran de afuera y llamadas a modelos de IA.
  • No tipes: scripts de una sola vez y variables obvias como total = 0.
  • Con la IA: las APIs de OpenAI y Anthropic convierten una clase tipada en un esquema y obligan al modelo a responder con ese formato. Eso se llama salidas estructuradas. Los agentes también leen tipos para saber cómo usar sus herramientas (tool calling y skills de agentes).

¿Para qué sirven los dos puntos (:) y la flecha (->)?

Para empezar no necesitas instalar nada. Todo se reduce a dos símbolos:

def obtener_saludo(nombre: str, veces: int = 1) -> str:
    return f"Hola, {nombre}! " * veces

Los dos puntos (:): qué tipo esperas

Con nombre: str le dices a quien lea el código (una persona, tu editor o un agente de IA) que nombre debe ser texto. Si el parámetro tiene un valor por defecto, va después del tipo: veces: int = 1.

También puedes tipar variables sueltas:

temperatura: float = 24.5
activo: bool = True

La flecha (->): qué devuelve la función

La flecha va justo antes de los dos puntos que abren la función. -> str: quiere decir “esta función devuelve texto”.

Si la función solo hace algo (imprimir, guardar un archivo) y no devuelve nada útil, su retorno es None:

def registrar_evento(mensaje: str) -> None:
    print(f"[LOG]: {mensaje}")

Ojo: Python no frena el programa por un tipo equivocado

Si vienes de Java, C++ o Rust, esto sorprende. Python no revisa las anotaciones cuando corre el código:

# La función espera texto, le pasas un número y corre igual:
obtener_saludo(nombre=123)  # devuelve 'Hola, 123! '

El tipo dice str, le pasaste un int y Python no protestó. Si algo falla, falla más adelante, por lo que hiciste con el dato, no por la anotación.

Entonces, ¿para qué escribirlas?

  1. Tu editor te avisa antes de ejecutar: VS Code, Cursor o PyCharm leen los tipos mientras escribes. Si llamas .upper() sobre algo que puede ser None, te lo marcan antes de correr el programa. En VS Code ese trabajo lo hace Pylance, pero su revisión de tipos viene apagada (off) por defecto; más abajo te digo cómo prenderla.
  2. Documentación que no se desactualiza: un comentario puede quedar viejo después de un cambio. Una firma tipada te dice qué entra y qué sale, y el verificador te avisa si deja de cumplirse.
  3. Revisión del proyecto completo: mypy o pyright se corren en la terminal o en tu pipeline de despliegue (CI/CD) para encontrar funciones que ya no cumplen lo que prometen en todo el repositorio.

¿Cuándo vale la pena tipar y cuándo no?

El error típico de quien descubre los tipos es tipar todo: cada variable de un for, cada script de 15 líneas. Tipar cada variable de un script de 15 líneas es como ponerle casco a una bicicleta estática.

Esta tabla te ayuda a decidir:

Tabla 1: Cuándo tipar en Python y cuándo no

Contexto¿Tipar?Por qué
Scripts de una sola vezNoSi vas a renombrar 100 archivos una sola vez (como en el script para renombrar archivos con Python), lo que importa es terminar rápido.
Variables obviasNocontador: int = 0 o cliente: str = "Ana" sobran. El verificador ya deduce que son un entero y un texto.
Funciones que se reutilizanSíSi una función vive en un módulo que otras partes usan, tipa sus parámetros y su retorno. Así nadie adivina qué recibe dentro de seis meses.
Datos que entran o salen (APIs, archivos, webhooks)SíLeer una base de datos, un JSON externo o un webhook sin tipos claros deja pasar datos dañados al resto del código.
Llamadas a modelos de IASí, siempreUn modelo puede responder distinto cada vez. Si no fijas el formato de salida, tarde o temprano rompe tu código.
Código que escribe un agente de IASí, siempreEl agente lee las firmas para saber qué puede cambiar sin romper el resto de la aplicación.

[!TIP] Regla práctica: tipa los bordes de tu código: lo que entra de afuera y lo que devuelven tus funciones. Adentro de cada función, deja que Python deduzca los tipos solo.


Cómo se tipa hoy: la sintaxis desde Python 3.10

Antes había que importar casi todo desde el módulo typing:

# Sintaxis antigua
from typing import List, Dict, Optional, Union

def procesar_datos(valores: List[Union[int, float]]) -> Optional[Dict[str, float]]:
    ...

Python 3.9 permitió escribir list y dict directamente (PEP 585). Python 3.10 agregó la barra vertical | para decir “esto o aquello” (PEP 604). Con las dos cosas ya no necesitas importar List, Dict, Union ni Optional:

# Sintaxis moderna (Python 3.10 en adelante)
def procesar_datos(valores: list[int | float]) -> dict[str, float] | None:
    ...

La barra vertical (|): esto o aquello

  • Un valor que puede ser entero o decimal: int | float.
  • Un valor que puede venir vacío: str | None. Antes se escribía Optional[str].

Literal: solo unas opciones

Muchas veces una función no acepta cualquier texto, solo unos valores concretos. Para eso está Literal:

from typing import Literal

EtapaEmbudo = Literal["tofu", "mofu", "bofu"]

def clasificar_articulo(visitas: int, intencion: EtapaEmbudo) -> str:
    return f"Contenido {intencion} con {visitas} lecturas."

Si alguien escribe clasificar_articulo(100, "conversion"), el editor lo marca: "conversion" no está en la lista.

TypedDict: diccionarios con llaves fijas

Con dict[str, str] solo dices de qué tipo son las llaves y los valores en general. Si quieres que el diccionario tenga llaves concretas, cada una con su tipo, usas TypedDict:

from typing import TypedDict

class MetadatosArticulo(TypedDict):
    slug: str
    palabras: int
    indexado: bool

def auditar_post(url: str) -> MetadatosArticulo:
    return {
        "slug": url.strip("/"),
        "palabras": 1420,
        "indexado": True
    }

Tu editor te autocompleta las llaves y el verificador te avisa si falta una. Python, igual que antes, no lo revisa al correr.


¿Por qué los tipos importan tanto con la IA?

Hasta aquí, el tipado es una herramienta clásica de programación. Con los modelos de lenguaje (LLM) ganó otro trabajo: es la forma de decirle al modelo qué formato debe tener su respuesta.

La razón es simple. Un modelo, por defecto, devuelve texto libre. Y el texto libre es difícil de leer para un programa: a veces llega el JSON solo y a veces con un “Claro, aquí está tu JSON” adelante.

flowchart TD
    PROMPT["<strong>Prompt</strong><br/>Lo que le pides al modelo"]
    LLM["<strong>Modelo de IA (LLM)</strong><br/>Genera la respuesta token por token"]

    TEXT["<strong>Texto libre</strong><br/>'Claro, aquí está el JSON: ...'<br/><em>Tu código puede no saber leerlo</em>"]
    SCHEMA["<strong>Esquema tipado</strong><br/>Clase de Pydantic convertida en JSON Schema<br/><em>El modelo solo puede escribir lo que encaja</em>"]
    DB["<strong>JSON con el formato pedido</strong><br/>Listo para tu base de datos o tu página"]

    PROMPT --> LLM
    LLM -- "Sin esquema" --> TEXT
    LLM -- "Con esquema" --> SCHEMA
    SCHEMA --> DB

Salidas estructuradas con Pydantic

OpenAI y Anthropic ofrecen salidas estructuradas (Structured Outputs) en sus APIs. Tú les pasas un esquema y el modelo solo puede generar respuestas que lo cumplan.

Ese esquema lo escribes como una clase tipada con Pydantic, una librería de Python que valida datos:

from pydantic import BaseModel, Field
from typing import Literal

class AnalisisBusquedaSEO(BaseModel):
    intencion_busqueda: Literal["informativa", "transaccional", "navegacional"]
    puntuacion_relevancia: float = Field(ge=0.0, le=1.0, description="Puntaje de 0 a 1")
    palabras_clave: list[str]
    resumen_bluf: str = Field(description="Resumen factual directo en 1 oración")

Con los SDK oficiales le pasas la clase directamente: client.responses.parse(text_format=...) en OpenAI o client.messages.parse(output_format=...) en Anthropic. Por debajo pasa esto:

  • El SDK convierte tu clase en un JSON Schema: un documento que describe qué campos lleva el JSON y de qué tipo es cada uno.
  • El modelo queda limitado a ese esquema: intencion_busqueda solo puede ser uno de los tres valores y puntuacion_relevancia tiene que ser un número.
  • Recibes un objeto de Python ya validado: listo para guardar en tu base de datos o mostrar en tu sitio.

Hay dos casos que la documentación de OpenAI menciona: si el modelo se niega a responder o si la respuesta se corta por el límite de tokens, puede no cumplir el esquema. Revisa esos casos en tu código.

Herramientas de un agente (tool calling)

Un agente de IA (en Claude Code, en Antigravity o en un servidor MCP que tú construyes) necesita herramientas para hacer cosas: leer archivos, llamar una API, comprimir una imagen. Que el modelo pida usar una de esas herramientas se llama tool calling.

En Python, una herramienta suele ser una función normal. El modelo no ve tu código: ve una descripción con el nombre, los parámetros y sus tipos. El SDK oficial de MCP para Python, por ejemplo, arma esa descripción a partir de la firma tipada. Por eso los tipos y el docstring tienen que ser claros:

def optimizar_imagen_web(
    ruta_origen: str,
    formato_destino: Literal["webp", "avif"] = "webp",
    calidad: int = 85
) -> dict[str, str | int]:
    """
    Comprime y convierte una imagen al formato web especificado.
    
    :param ruta_origen: Ruta absoluta o relativa de la imagen fuente.
    :param formato_destino: Formato de salida comprimido ('webp' o 'avif').
    :param calidad: Nivel de compresión entre 1 y 100.
    :return: Diccionario con la nueva ruta y el tamaño final en bytes.
    """
    # Lógica interna de compresión...
    return {"ruta": "foto.webp", "bytes": 45000}

Con formato_destino: Literal["webp", "avif"] = "webp", el modelo sabe que no debe pedir "png" ni "gif". Y si activas el modo estricto (strict: true en la API de Anthropic, por ejemplo), la API valida que cada llamada cumpla el esquema. Un tipo así es una instrucción mucho más precisa que una frase en el prompt.

Una red de seguridad para el vibe coding

El vibe coding es programar pidiéndole cambios a un agente de IA. Como explico en la guía sobre qué es Git en la era del vibe coding, es rápido pero tiene sus riesgos. El agente puede cambiar una función y, sin avisarte, hacer que devuelva una lista donde antes devolvía un diccionario.

  • Sin tipos: te enteras cuando un usuario real hace clic y algo se rompe en producción.
  • Con tipos y un verificador: si el agente corre pyright o mypy después de cada cambio, el error aparece de inmediato y el agente lo puede corregir antes de que apruebes el cambio o hagas git commit.

Para que el agente lo corra siempre, pídeselo en sus instrucciones, por ejemplo en el archivo AGENTS.md o en una skill.


Cómo se conecta con otros temas del blog


Cómo empezar hoy

  1. Usa Python 3.10 o más nuevo: escribe list[str], dict[str, int] y str | None sin importar nada de typing.
  2. Tipa los bordes: entradas y salidas de tus funciones principales, endpoints y modelos de datos. Lo de adentro, déjalo.
  3. Usa Literal para opciones fijas: si un parámetro solo acepta 2 o 3 valores, no lo dejes como str a secas.
  4. Usa Pydantic para hablar con un modelo de IA: no le pidas “respóndeme en JSON con este formato” en el prompt. Pásale un esquema tipado con las salidas estructuradas de la API.
  5. Prende la revisión de tipos en tu editor: en VS Code, cambia python.analysis.typeCheckingMode a "basic". Desde ahí el editor te avisa mientras escribes.

Fuentes

Consultadas el 27 de octubre de 2026.

RUTA DE LECTURA RECOMENDADA

Más allá del código: estrategia, escalabilidad e impacto

Lecturas clave para conectar la arquitectura técnica con el valor de negocio y rentabilidad.

EXPLORAR CATEGORÍA COMPLETA→