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". .envsiempre en.gitignore, verificado ANTES del primer commit (git statusno 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 bloquewith— mismos parámetros quecreate().stream.text_stream— iterador que entrega fragmentos de texto según se generan. Elend=""evita saltos de línea entre fragmentos yflush=Truefuerza 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:
- Reintenta solo los errores transitorios (429 y 5xx) un par de veces, con espera exponencial. Muchos errores se resuelven sin que te enterés.
- 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 mensajeuserhuérfano — y el siguiente turno tendría dosuserseguidos → 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 amax_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
- Documentación de la plataforma: platform.claude.com/docs — referencia completa de la API de Messages, streaming, visión, herramientas.
- La consola: console.anthropic.com — tus keys, tu saldo, tu uso.
- SDK de Python en GitHub: buscá
anthropic-sdk-python— ejemplos oficiales ejecutables. - Capítulo 5 de este libro — ahora que tu programa funciona, toca que funcione barato: costos, caching, batches y límites.