Tu primera app con la API

"Usar la app es manejar el carro. Usar la API es tener el motor para construir el vehículo que vos querás."

Qué vas a aprender en este capítulo

Este es el capítulo donde pasás de usuario a constructor. La API de Anthropic permite que tus propios programas en Python le hablen a Claude: mandan un request, reciben una respuesta, y hacen con ella lo que tu código decida. Sobre esa base se construye todo lo demás — chatbots, analizadores de documentos, clasificadores, asistentes a la medida.

Vas a recorrer el camino completo: crear tu API key (y protegerla como se debe), instalar el SDK, tu primer request explicado línea por línea, la anatomía de request y response, conversaciones con memoria, streaming y manejo de errores. El capítulo cierra con un programa completo: un tutor de consola con historial — la tercera ala de tu centro de productividad.

Prerequisito real: Python básico — variables, funciones, listas, diccionarios, bucles, try/except. Si eso te suena a chino, pasá primero por Programación I.

4.1 La API key: tu llave (y tu responsabilidad)

💡 Intuición

Una API key es la llave de tu cuenta: una cadena secreta que identifica tus requests y los carga a TU facturación. Quien tenga la llave puede gastar tu saldo — como quien tiene tu tarjeta puede comprar a tu nombre.

De ahí la regla que va a repetirse en este capítulo hasta el cansancio: la llave nunca va dentro del código. Va en el llavero (una variable de entorno), y el código la toma de ahí.

🛠️ En la práctica — crear y guardar la key

Paso 1 — Crear la cuenta y la key. Entrá a console.anthropic.com, creá tu cuenta de desarrollador y agregá un saldo pequeño (con $5 sobra de sobra para todo este libro — los requests de práctica cuestan fracciones de centavo). En la sección de API keys, generá una nueva. Copiala de inmediato: por seguridad solo se muestra completa una vez.

Paso 2 — Guardarla como variable de entorno. El SDK busca automáticamente la variable ANTHROPIC_API_KEY.

En Linux/macOS (o WSL), agregala a tu archivo de perfil de shell:

# Agregá esta línea a ~/.bashrc o ~/.zshrc
export ANTHROPIC_API_KEY="tu-key-aqui"

# Recargá el perfil y verificá
source ~/.bashrc
echo $ANTHROPIC_API_KEY

En Windows (PowerShell):

[Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "tu-key-aqui", "User")
# Cerrá y reabrí la terminal para que tome efecto

Paso 3 — Para proyectos, un .env ignorado por git. Alternativa común por proyecto: un archivo .env con ANTHROPIC_API_KEY=... cargado con la librería python-dotenv — y la línea .env en tu .gitignore antes del primer commit.

⚠️ Trampa común

La key en el código, y el código en GitHub. La secuencia del desastre, vista mil veces: (1) "solo para probar", escribís api_key="sk-ant-..." en el script; (2) días después subís el proyecto a GitHub para entregarlo o mostrarlo; (3) bots que escanean GitHub las 24 horas encuentran la key en minutos — no horas, minutos; (4) tu saldo amanece en cero, gastado por extraños.

Reglas absolutas:

  • La key jamás aparece en un archivo .py, ni en un notebook, ni en un README, ni "comentada".
  • .env siempre en .gitignore, verificado ANTES del primer commit (git status no debe listarlo).
  • Si una key tocó un commit alguna vez — aunque borrés el archivo después — considerala quemada: revocala en la consola y generá otra. El historial de git recuerda todo.

En el capítulo 5 vas a ver el protocolo completo de qué hacer si se te filtró una.

4.2 Instalar el SDK y el primer request

🛠️ En la práctica

El SDK (Software Development Kit) oficial de Python envuelve la API HTTP en clases y funciones cómodas:

pip install anthropic

Tu primer programa con Claude, completo:

import anthropic

client = anthropic.Anthropic()  # lee ANTHROPIC_API_KEY del entorno
respuesta = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system="Sos un tutor de programación paciente.",  # opcional
    messages=[{"role": "user", "content": "Explicame qué es una variable."}],
)
print(respuesta.content[0].text)

Guardalo como primer_request.py, corré python primer_request.py, y leé la explicación de Claude en tu terminal. Acabás de hacer tu primer request. Ahora desarmémoslo línea por línea.

📐 Fundamento — línea por línea

import anthropic

Importa el SDK. Si falla, el pip install no corrió en el mismo entorno donde ejecutás Python.

client = anthropic.Anthropic()

Crea el cliente: el objeto que sabe hablar con los servidores de Anthropic. Sin argumentos, busca tu key en la variable de entorno ANTHROPIC_API_KEY — exactamente lo que queremos: el código no conoce la key. Se crea una vez y se reutiliza para todos los requests del programa.

respuesta = client.messages.create(

El método central de toda la API: crear un "message" — mandás una conversación, el modelo genera el siguiente mensaje. Prácticamente todo lo que hagas con la API pasa por aquí.

    model="claude-opus-4-8",

Qué modelo responde, por su ID exacto (tabla del cap. 1). El ID va tal cual, sin sufijos de fecha. Elegir modelo = elegir el balance capacidad/precio de este request.

    max_tokens=1024,

Obligatorio: techo de tokens de la respuesta. Es un freno de seguridad (y de presupuesto): la generación se corta ahí aunque el modelo no haya terminado. 1024 tokens ≈ 780 palabras.

    system="Sos un tutor de programación paciente.",

El system prompt (opcional): instrucciones de comportamiento que pesan más que cualquier mensaje del usuario. Define rol, tono, reglas. Es el equivalente programático de las "instrucciones de Proyecto" del capítulo 2.

    messages=[{"role": "user", "content": "Explicame qué es una variable."}],
)

La conversación: una lista de mensajes, cada uno con role ("user" o "assistant") y content. Aquí hay un solo mensaje; en la sección 4.4 esta lista va a crecer. Regla de la API: los roles alternan, y el primero siempre es user.

print(respuesta.content[0].text)

La respuesta llega como una lista de bloques de contenido (por eso el [0]) — en uso básico hay un solo bloque de texto. .text es el texto generado.

4.3 Anatomía del response (y la naturaleza stateless)

📐 Fundamento

El objeto que devuelve messages.create() trae más que texto:

respuesta.content        # lista de bloques; respuesta.content[0].text es el texto
respuesta.model          # qué modelo respondió
respuesta.stop_reason    # POR QUÉ terminó de generar (clave, ver abajo)
respuesta.usage          # cuántos tokens consumiste (¡la factura!)

usage — tu contador de gasto. Cada response te dice exactamente qué consumió:

print(respuesta.usage.input_tokens)   # tokens de entrada procesados
print(respuesta.usage.output_tokens)  # tokens generados

Con la tabla de precios del capítulo 1 calculás el costo exacto de cada request — en el capítulo 5 esto se convierte en sistema; por ahora, sabé que el dato siempre viene.

stop_reason — por qué dejó de escribir:

Valor Significado Qué hacés
"end_turn" Terminó naturalmente Nada — caso feliz
"max_tokens" Se cortó por tu límite La respuesta está incompleta: subí max_tokens o pedí salidas más cortas
"tool_use" Quiere usar una herramienta Tema de tool use (avanzado, no lo cubrimos acá)
"refusal" Se negó por seguridad Revisá el pedido; no insistas con reintentos

La API es stateless. Este punto define todo lo que sigue: el servidor no recuerda nada entre requests. Cada llamada a messages.create() es un universo nuevo: el modelo solo sabe lo que va en la lista messages de ESE request. El "chat con memoria" que conocés de claude.ai no es magia del servidor — es la app reenviando todo el historial cada vez. En la próxima sección lo construís vos.

⚠️ Trampa común

Ignorar stop_reason y publicar respuestas cortadas. Pediste un resumen largo con max_tokens=300, la respuesta termina en "...y por lo tanto la conclusión más importante es" — y tu programa la muestra como si estuviera completa. El modelo no avisa en el texto: avisa en stop_reason == "max_tokens", y si no lo chequeás, no te enterás.

if respuesta.stop_reason == "max_tokens":
    print("⚠️ Respuesta incompleta: subí max_tokens.")

Cuatro líneas que salvan la calidad de cualquier app. Ponelas siempre.

4.4 Conversación multi-turno: la memoria la ponés vos

💡 Intuición

Hablar con la API es como escribirse con alguien que tiene amnesia total pero lee rapidísimo: cada carta que le mandás debe incluir toda la correspondencia anterior, porque él no guarda copias. Mientras le reenviés el legajo completo, la conversación fluye perfecta — el día que mandés solo la última carta, te va a contestar como a un desconocido.

Tu programa es el archivador: acumula las cartas (mensajes) y reenvía el legajo (historial) completo en cada request.

🛠️ En la práctica — chat con memoria en 25 líneas

import anthropic

client = anthropic.Anthropic()
historial = []  # acá vive la "memoria"

print("Chat con Claude (escribí 'salir' para terminar)\n")

while True:
    pregunta = input("Vos: ")
    if pregunta.lower() == "salir":
        break

    # 1. Agregar el mensaje del usuario al historial
    historial.append({"role": "user", "content": pregunta})

    # 2. Mandar TODO el historial (no solo la última pregunta)
    respuesta = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        messages=historial,
    )
    texto = respuesta.content[0].text

    # 3. Agregar la respuesta al historial — clave para el próximo turno
    historial.append({"role": "assistant", "content": texto})

    print(f"\nClaude: {texto}\n")

Probalo: decile tu nombre, preguntá dos cosas más, y después preguntale "¿cómo me llamo?". Se acuerda — porque tu lista historial se acuerda.

Fijate en la mecánica de la lista: user, assistant, user, assistant... siempre alternando, siempre empezando por user. Si rompés la alternancia (dos user seguidos, o empezar por assistant), la API devuelve error 400.

El costo escondido del historial: cada turno reenvía todo lo anterior como tokens de entrada — el turno 20 de una conversación larga es mucho más caro que el turno 2. Por eso las conversaciones largas se truncan o resumen en apps reales (cap. 5).

⚠️ Trampa común

"El bot no se acuerda de nada." El error más clásico del principiante: mandar solo el último mensaje.

# ❌ MAL: cada request es una conversación nueva de un solo mensaje
respuesta = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{"role": "user", "content": pregunta}],  # ¿y lo anterior?
)

Síntoma inconfundible: le decís tu nombre y al turno siguiente no lo sabe; le pedís "explicá eso de otra forma" y pregunta "¿eso cuál?". No es un bug del modelo — es que tu código no le mandó la conversación. La API es stateless: la memoria es una lista en TU programa, y se reenvía completa (mensajes de user Y de assistant) en cada request.

4.5 Streaming: respuestas que fluyen

📐 Fundamento

Con messages.create() esperás a que la respuesta entera esté generada antes de ver un carácter. Para respuestas largas eso significa: usuario mirando una pantalla congelada 30 segundos y — peor — riesgo de timeout en la conexión. Con streaming, los tokens llegan a medida que se generan, como en claude.ai.

import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-4-8",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Explicame la historia de Internet, con detalle."}],
) as stream:
    for texto in stream.text_stream:
        print(texto, end="", flush=True)

mensaje_final = stream.get_final_message()
print(f"\n\n[Tokens: {mensaje_final.usage.output_tokens}]")

Las piezas:

  • client.messages.stream(...) en un bloque with — mismos parámetros que create().
  • stream.text_stream — iterador que entrega fragmentos de texto según se generan. El end="" evita saltos de línea entre fragmentos y flush=True fuerza a la terminal a mostrarlos al instante.
  • stream.get_final_message() — al terminar, el objeto completo de siempre: usage, stop_reason, todo. Streaming no te quita información; te la adelanta.

¿Cuándo usarlo? Para salidas largas el streaming es obligatorio — sin él, la conexión puede agotar el tiempo de espera antes de que termine la generación. Regla práctica: interfaz interactiva o max_tokens alto → streaming; script batch de respuestas cortas → create() normal alcanza.

4.6 Manejo de errores: programar para el mundo real

📐 Fundamento

Los requests fallan: key mal configurada, demasiados requests por minuto, servidores saturados. Un programa serio distingue los casos. Los errores principales:

Código Excepción del SDK Significado Qué hacer
401 anthropic.AuthenticationError Key inválida o ausente Revisar la variable de entorno; no reintentar
429 anthropic.RateLimitError Límite de requests/tokens por minuto Esperar (header retry-after) y reintentar
529 anthropic.APIStatusError (overloaded) API sobrecargada Reintentar con espera creciente
400 anthropic.BadRequestError Request mal armado (roles que no alternan, parámetro inválido...) Corregir el código; reintentar no sirve

Dos cosas que el SDK ya hace por vos:

  1. Reintenta solo los errores transitorios (429 y 5xx) un par de veces, con espera exponencial. Muchos errores se resuelven sin que te enterés.
  2. Excepciones tipadas: en lugar de revisar códigos a mano, capturás clases específicas — todas heredan de anthropic.APIError.
import anthropic

client = anthropic.Anthropic()

def preguntar(historial):
    """Un request con manejo de errores digno de producción."""
    try:
        return client.messages.create(
            model="claude-opus-4-8",
            max_tokens=1024,
            messages=historial,
        )
    except anthropic.AuthenticationError:
        print("Error: API key inválida. Revisá ANTHROPIC_API_KEY.")
        raise SystemExit(1)        # sin key válida no hay nada que hacer
    except anthropic.RateLimitError:
        print("Rate limit alcanzado incluso tras reintentos. Esperá un minuto.")
        return None
    except anthropic.APIError as e:
        print(f"Error de la API ({e.status_code}): {e.message}")
        return None

El orden de los except importa: de lo específico a lo general. APIError al final, como red para todo lo no contemplado. Y nota el criterio: el 401 aborta el programa (reintentar es inútil), los transitorios devuelven None y dejan que el programa siga vivo.

4.7 Programa completo: tutor de consola con historial

🛠️ En la práctica — Avance del proyecto: el tutor

Todo el capítulo junto en un programa real: un tutor de estudio por consola, con memoria, streaming, manejo de errores y contador de tokens. Guardalo como tutor.py.

"""Tutor de consola con Claude — proyecto del capítulo 4.

Uso:  python tutor.py
Requiere:  pip install anthropic  +  ANTHROPIC_API_KEY en el entorno.
"""

import anthropic

MODELO = "claude-opus-4-8"
MAX_TOKENS = 2048

SYSTEM = (
    "Sos un tutor universitario paciente para estudiantes de El Salvador. "
    "Explicá con claridad y con ejemplos locales cuando sumen. "
    "Si te piden resolver tareas, guiá con preguntas antes de dar la solución. "
    "Si no estás seguro de un dato, decilo explícitamente."
)

def conversar(client, historial):
    """Manda el historial completo y muestra la respuesta en streaming.

    Devuelve (texto, usage) o (None, None) si hubo error recuperable.
    """
    try:
        with client.messages.stream(
            model=MODELO,
            max_tokens=MAX_TOKENS,
            system=SYSTEM,
            messages=historial,
        ) as stream:
            print("\nTutor: ", end="")
            for fragmento in stream.text_stream:
                print(fragmento, end="", flush=True)
            print()
            final = stream.get_final_message()

        if final.stop_reason == "max_tokens":
            print("\n[Aviso: respuesta cortada por max_tokens]")
        return final.content[0].text, final.usage

    except anthropic.AuthenticationError:
        print("\nAPI key inválida. Configurá ANTHROPIC_API_KEY y volvé a correr.")
        raise SystemExit(1)
    except anthropic.RateLimitError:
        print("\nDemasiados requests. Esperá un minuto y repetí la pregunta.")
        return None, None
    except anthropic.APIError as e:
        print(f"\nError de la API: {e.message}")
        return None, None

def main():
    client = anthropic.Anthropic()
    historial = []
    total_entrada, total_salida = 0, 0

    print("=== Tutor de estudio ===")
    print("Comandos: 'salir' termina | 'reset' borra la conversación\n")

    while True:
        pregunta = input("Vos: ").strip()
        if not pregunta:
            continue
        if pregunta.lower() == "salir":
            break
        if pregunta.lower() == "reset":
            historial = []
            print("[Conversación reiniciada]\n")
            continue

        historial.append({"role": "user", "content": pregunta})
        texto, usage = conversar(client, historial)

        if texto is None:
            historial.pop()  # el request falló: sacamos la pregunta huérfana
            continue

        historial.append({"role": "assistant", "content": texto})
        total_entrada += usage.input_tokens
        total_salida += usage.output_tokens
        print()

    print("\n=== Sesión terminada ===")
    print(f"Tokens de entrada: {total_entrada} | de salida: {total_salida}")

if __name__ == "__main__":
    main()

Detalles de diseño que valen la pena notar:

  • historial.pop() tras un fallo: si el request murió, la pregunta quedaría como mensaje user huérfano — y el siguiente turno tendría dos user seguidos → error 400. Limpiar el historial en fallos es de los bugs sutiles más comunes en chats caseros.
  • El comando reset: vaciar la lista = nueva conversación. Así de literal es la naturaleza stateless.
  • El contador de tokens: todavía no calcula dólares — ese es exactamente el punto de partida del capítulo 5, donde el tutor gana presupuesto, caché y conciencia de costos.

Tu centro de productividad ya tiene sus tres alas: Proyectos para estudiar, Claude Code para programar, y tu primera herramienta propia con la API.

Resumen visual

Mermaid

sequenceDiagram participant U as Usuario participant P as Tu programa (tutor.py) participant A as API de Anthropic U->>P: escribe pregunta P->>P: historial.append(user) P->>A: messages.stream(model, max_tokens,
system, TODO el historial) A-->>P: tokens en streaming P-->>U: texto fluyendo en pantalla A->>P: mensaje final (usage, stop_reason) P->>P: historial.append(assistant) Note over P: la memoria vive en TU lista,
no en el servidor (stateless)

Pieza Lo esencial
API key En ANTHROPIC_API_KEY, jamás en el código; filtrada = revocada
messages.create() model + max_tokens + messages (+ system opcional)
Response content[0].text, usage (tokens), stop_reason (chequearlo)
Stateless El servidor no recuerda: reenviás el historial completo cada vez
Multi-turno Lista alternando user/assistant, primero siempre user
Streaming messages.stream() + text_stream; obligatorio para salidas largas
Errores Excepciones tipadas; el SDK reintenta 429/5xx solo

Ejercicios

✏️ Ejercicio 1 — Lectura de response

Un request devolvió: usage.input_tokens = 850, usage.output_tokens = 2048, stop_reason = "max_tokens", y el programa usó max_tokens=2048. (a) ¿Qué pasó con la respuesta? (b) ¿Qué coincidencia de números lo delata? (c) ¿Qué dos soluciones tenés?

✅ Solución
  • (a) La respuesta quedó incompleta: el modelo seguía generando cuando chocó con el techo.
  • (b) output_tokens (2048) es exactamente igual a max_tokens (2048) — la firma típica del corte. Una respuesta que termina naturalmente casi nunca clava el límite exacto.
  • (c) Subir max_tokens (p. ej. a 4096) y reintentar, o rediseñar el prompt para pedir una salida más corta/dividida ("resumí en 3 puntos" en vez de "explicá con todo detalle"). Si las salidas van a ser largas de verdad, además, streaming.

✏️ Ejercicio 2 — Cazá el bug del historial

Este chat "pierde la memoria" y a veces revienta con error 400. Encontrá los DOS bugs:

historial = []
while True:
    pregunta = input("Vos: ")
    historial.append({"role": "user", "content": pregunta})
    respuesta = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        messages=[historial[-1]],
    )
    print(respuesta.content[0].text)
✅ Solución

Bug 1 — messages=[historial[-1]]: manda solo el último mensaje, no el historial. El modelo nunca ve los turnos anteriores → amnesia total. Debe ser messages=historial.

Bug 2 — nunca se agrega la respuesta del asistente: falta historial.append({"role": "assistant", "content": respuesta.content[0].text}) después del request. Sin él, aunque arreglés el bug 1, el historial queda como user, user, user... — roles que no alternan → error 400 en el segundo turno.

Los dos bugs juntos enseñan la lección completa: memoria = historial completo + ambos roles.

✏️ Ejercicio 3 — Clasificador de errores

Tu app de consola lanza estas excepciones en distintos momentos. Para cada una: ¿cuál es la causa probable y reintentar tiene sentido? (a) anthropic.AuthenticationError al primer request del día; (b) anthropic.RateLimitError mientras corrés un script que procesa 500 archivos en bucle; (c) anthropic.BadRequestError: roles must alternate; (d) error 529 a las 9:00 a.m. de un lunes.

✅ Solución
  • (a) Key inválida/ausente — quizás abriste una terminal nueva sin la variable cargada, o revocaste la key. Reintentar es inútil: corregí la configuración.
  • (b) Estás mandando requests más rápido de lo que tu límite permite — esperable en un bucle de 500 ítems. Reintentar con espera sí funciona (el SDK ya lo intentó); la solución de fondo es espaciar requests o usar la Batches API (cap. 5).
  • (c) Bug en TU código: el historial tiene dos mensajes seguidos del mismo rol (mirá el patrón del historial.pop() del tutor). Reintentar no sirve — el mismo request mal armado fallará igual.
  • (d) Sobrecarga de la API (hora pico). Reintentar con espera creciente sí; si persiste, probá más tarde o con otro modelo menos cargado.

✏️ Ejercicio 4 — Extendé el tutor

Agregale al tutor.py un comando tokens que muestre el acumulado de la sesión sin salir, y un truncado simple: si el historial supera los 40 mensajes, conservá solo los últimos 20 (¿por qué un número par? ¿qué riesgo tiene truncar así?).

✅ Solución

El comando, dentro del while, junto a los otros comandos:

if pregunta.lower() == "tokens":
    print(f"[Entrada: {total_entrada} | Salida: {total_salida}]\n")
    continue

El truncado, justo antes del request:

if len(historial) > 40:
    historial = historial[-20:]

¿Por qué par? El historial alterna user/assistant; un recorte de longitud par que arranca con los últimos mensajes mantiene la alternancia y el primer mensaje del recorte es user (porque la lista completa empieza en user y los pares se preservan). Con un corte impar arrancarías en assistant → error 400.

El riesgo: se pierde contexto viejo — si le dijiste tu nombre en el mensaje 3 y vas por el 50, ya no lo sabe. La alternativa más fina es resumir lo viejo en un mensaje antes de descartarlo (lo tocamos en el cap. 5).

Para profundizar