Módulo 6: Debugging con Claude Code
Debugging Sistemático
Debugging Sistemático
Descripción de la cápsula
La diferencia entre un developer junior y uno senior debuggeando no es inteligencia — es proceso. El junior ve un error y empieza a cambiar cosas al azar: "¿Qué pasa si pongo un try/except aquí? ¿Y si cambio esta variable?" El senior sigue un proceso disciplinado que lo lleva a la causa raíz en menos tiempo y con menos frustración.
En esta cápsula vas a aprender ese proceso: reproducir → aislar → diagnosticar → fix → verificar. Cinco pasos, siempre en ese orden, sin saltarte ninguno. Y vas a ver cómo Claude Code encaja en cada paso — no como el proceso completo, sino como una herramienta dentro del proceso. Al final, vas a aplicar el proceso completo a un bug realista que solo aparece con ciertos inputs.
El Proceso de 5 Pasos
Paso 1: REPRODUCIR
Pregunta que respondes: "¿Puedo hacer que el error ocurra de forma consistente?"
Si no puedes reproducir el bug, no puedes confirmar que tu fix funciona. Este paso es la base de todo.
Qué hacer
- Identifica las condiciones exactas: qué endpoint, qué datos, qué usuario, qué secuencia
- Reproduce el error al menos 2 veces para confirmar que es consistente
- Documenta los pasos de reproducción (los necesitarás para el paso 5)
Ejemplo
# Reproducción del bug
# Endpoint: POST /api/tasks
# Body: {"title": "Test", "due_date": "2026-02-30"}
# Esperado: Error de validación (fecha inválida)
# Obtenido: Error 500 (crash del servidor)
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title": "Test", "due_date": "2026-02-30"}'
# Resultado: 500 Internal Server Error
# Reproducido: Sí, consistente en 3/3 intentos
Qué pasa si no puedes reproducir
Si el error es intermitente:
- ✅ Busca patrones: ¿ocurre a ciertas horas? ¿Con ciertos usuarios? ¿Después de cierta carga?
- ✅ Agrega más logging para capturar el estado cuando ocurra de nuevo
- ✅ Revisa logs históricos: ¿hay un patrón que no estás viendo?
- ✅ Considera condiciones de carrera (race conditions): ¿dos requests simultáneos?
Dónde Claude Code ayuda en este paso
Limitado. Claude Code no puede ejecutar tu aplicación ni hacer requests. Pero puede ayudarte a:
- Diseñar un script de reproducción basado en la descripción del error
- Sugerir qué variables/condiciones probar para reproducir un bug intermitente
Tengo un bug que aparece a veces cuando creo tareas. El error es
un 500 pero no aparece con todos los inputs. ¿Qué variaciones de
input debería probar para identificar el patrón?
Modelo:
"""python
class TaskCreate(BaseModel):
title: str
due_date: Optional[datetime] = None
priority: str = "medium"
tags: Optional[list[str]] = None
assignee_id: Optional[int] = None
"""
Paso 2: AISLAR
Pregunta que respondes: "¿Cuál es el input mínimo que causa el error?"
Una vez que puedes reproducir el bug, simplifícalo. Reduce el input hasta encontrar la versión más pequeña que todavía causa el error. Esto te dice exactamente qué parte del input es problemática.
La técnica de reducción
Input original que falla:
{
"title": "Tarea importante del proyecto X",
"due_date": "2026-02-30",
"priority": "high",
"tags": ["backend", "urgent"],
"assignee_id": 42,
"description": "Esta tarea necesita completarse antes del sprint"
}
Paso 1: Quitar campos opcionales uno por uno
Sin tags → ¿Falla? Sí
Sin assignee_id → ¿Falla? Sí
Sin description → ¿Falla? Sí
Sin priority → ¿Falla? Sí
Sin due_date → ¿Falla? NO ← ¡Encontrado!
Input mínimo que causa el error:
{
"title": "Test",
"due_date": "2026-02-30"
}
Conclusión: El bug está relacionado con due_date.
Reducción más fina
¿Es la fecha en sí o el formato?
"due_date": "2026-02-30" → Falla (30 de febrero no existe)
"due_date": "2026-02-28" → Funciona
"due_date": "2026-04-31" → Falla (31 de abril no existe)
"due_date": "2026-13-01" → Falla (mes 13 no existe)
"due_date": "2026-03-15" → Funciona
Conclusión: El bug ocurre con fechas inválidas que
el parser no maneja correctamente.
Dónde Claude Code ayuda en este paso
Moderado. Claude Code puede sugerirte qué inputs probar para aislar un bug:
Encontré un bug que aparece con ciertos valores de due_date pero
no con otros. "2026-02-30" falla, "2026-02-28" funciona. ¿Qué
otros valores debería probar para confirmar que el problema es
con fechas inválidas?
Paso 3: DIAGNOSTICAR
Pregunta que respondes: "¿Por qué falla con este input?"
Este es el paso donde Claude Code brilla. Ya tienes:
- El input mínimo que causa el error
- El stack trace o log del error
- El código relevante
Ahora pasas todo esto a Claude Code para obtener una hipótesis de diagnóstico.
Ejemplo completo
Mi endpoint POST /api/tasks falla con un 500 cuando paso
una fecha inválida como "2026-02-30". Funciona con fechas
válidas como "2026-03-15".
Stack trace:
"""
Traceback (most recent call last):
File "/app/routers/tasks.py", line 18, in create_task
task_data = TaskCreate(**body)
File "/app/services/task_service.py", line 32, in create
parsed_date = datetime.strptime(data.due_date, "%Y-%m-%d")
ValueError: day is out of range for month
"""
Código del servicio:
"""python
from datetime import datetime
from typing import Optional
class TaskService:
def create(self, data) -> dict:
parsed_date = datetime.strptime(data.due_date, "%Y-%m-%d")
# ... resto del código
"""
Input que funciona: {"title": "Test", "due_date": "2026-03-15"}
Input que falla: {"title": "Test", "due_date": "2026-02-30"}
¿Por qué strptime no maneja esto como un error de validación
en lugar de un crash?
Evaluando la respuesta de Claude Code
Claude Code probablemente dirá:
datetime.strptime()intenta crear un objeto datetime con la fecha proporcionada. "2026-02-30" es una fecha que no existe (febrero solo tiene 28 o 29 días), así que Python lanzaValueError. El problema es que este error no está siendo capturado — no hay try/except ni validación previa.Sugiero validar la fecha antes de procesarla, o usar Pydantic's
datetimetype que hace la validación automáticamente.
Tu evaluación del diagnóstico:
- ✅ Correcto:
strptimelanzaValueErrorcon fechas inválidas - ✅ Correcto: No hay manejo de error
- ✅ La sugerencia de usar el tipo
datetimede Pydantic es buena — mueve la validación al schema
Lo que NO debes hacer en este paso
- ❌ Aceptar el primer diagnóstico sin verificarlo
- ❌ Aplicar el fix sin antes entender por qué funciona
- ❌ Ignorar si Claude Code sugiere algo que cambia la lógica de negocio
Paso 4: FIX
Pregunta que respondes: "¿Cuál es la solución correcta que no introduce bugs nuevos?"
Con el diagnóstico verificado, aplicas el fix. Pero hay dos tipos de fix, y elegir el incorrecto es un error común:
Fix de parche vs fix de raíz
# Fix de parche: atrapa el error pero no arregla la causa
def create(self, data) -> dict:
try:
parsed_date = datetime.strptime(data.due_date, "%Y-%m-%d")
except ValueError:
parsed_date = None # ← ¿Es correcto crear una tarea sin fecha?
# Fix de raíz: valida antes de procesar
from pydantic import BaseModel, field_validator
from datetime import datetime, date
from typing import Optional
class TaskCreate(BaseModel):
title: str
due_date: Optional[date] = None
@field_validator('due_date', mode='before')
@classmethod
def validate_due_date(cls, v):
if v is None:
return v
if isinstance(v, str):
try:
return date.fromisoformat(v)
except ValueError:
raise ValueError(f"Invalid date format: {v}. Use YYYY-MM-DD with valid values.")
return v
El fix de raíz:
- ✅ Valida en el schema (antes de que llegue al servicio)
- ✅ Devuelve un error claro al usuario (422 con mensaje descriptivo)
- ✅ No permite crear tareas con estados inválidos
Cómo pedir un fix a Claude Code
El bug está diagnosticado: datetime.strptime no maneja fechas
inválidas. Necesito un fix que:
1. Valide la fecha en el schema Pydantic (no en el service)
2. Devuelva un 422 con mensaje claro si la fecha es inválida
3. Acepte None como valor válido (campo opcional)
4. No cambie el comportamiento para fechas válidas
Código actual del schema:
"""python
class TaskCreate(BaseModel):
title: str
due_date: Optional[str] = None
"""
Paso 5: VERIFICAR
Pregunta que respondes: "¿El fix resuelve el problema original sin crear problemas nuevos?"
Este es el paso que más se salta — y el más importante. Verificar no es solo "ya no crashea":
Checklist de verificación
□ ¿El input que causaba el error ahora funciona correctamente?
→ curl con "2026-02-30" → 422 con mensaje claro ✅
□ ¿El input que funcionaba ANTES sigue funcionando?
→ curl con "2026-03-15" → 201 Created ✅
□ ¿Edge cases relacionados funcionan?
→ curl con due_date=null → 201 Created (sin fecha) ✅
→ curl sin campo due_date → 201 Created (sin fecha) ✅
→ curl con "not-a-date" → 422 con mensaje claro ✅
→ curl con "2026-02-29" → 422 (2026 no es bisiesto) ✅
□ ¿El fix no rompe otros endpoints que usan la misma clase?
→ PATCH /api/tasks/{id} con due_date → funciona ✅
□ ¿El mensaje de error es útil para el cliente?
→ "Invalid date format: 2026-02-30. Use YYYY-MM-DD
with valid values." ✅
Dónde Claude Code ayuda en este paso
Moderado. Puedes pedirle que te sugiera edge cases para probar:
Acabo de arreglar un bug de validación de fechas en un
endpoint FastAPI. ¿Qué edge cases debería probar para
asegurarme de que el fix es completo?
El campo es: due_date: Optional[date] = None
El Proceso Completo en Acción: Un Caso Real
Vamos a recorrer los 5 pasos con un bug más complejo.
El reporte del bug
"El endpoint GET /api/tasks devuelve tareas duplicadas. A veces un task aparece 2 o 3 veces en la respuesta."
Paso 1: REPRODUCIR
# Intento 1: request básico
curl http://localhost:8000/api/tasks
# Resultado: 15 tasks, sin duplicados ← No se reproduce
# Intento 2: con filtro de status
curl "http://localhost:8000/api/tasks?status=pending"
# Resultado: 8 tasks, sin duplicados ← No se reproduce
# Intento 3: con múltiples filtros
curl "http://localhost:8000/api/tasks?status=pending&priority=high"
# Resultado: 5 tasks, task #12 aparece 2 veces ← ¡Reproducido!
# Confirmo: lo corro 3 veces más
# Resultado: siempre duplica task #12 con estos filtros
Documentación de reproducción:
- Endpoint: GET /api/tasks?status=pending&priority=high
- Resultado: task #12 aparece duplicada
- Consistente: sí (3/3)
Paso 2: AISLAR
# ¿Es el filtro de status?
curl "http://localhost:8000/api/tasks?status=pending"
# Sin duplicados
# ¿Es el filtro de priority?
curl "http://localhost:8000/api/tasks?priority=high"
# Sin duplicados
# ¿Es la combinación de ambos?
curl "http://localhost:8000/api/tasks?status=pending&priority=high"
# ¡Duplicados!
# ¿Pasa con otras combinaciones?
curl "http://localhost:8000/api/tasks?status=completed&priority=low"
# Sin duplicados ← Hmm, ¿solo con pending + high?
# ¿Es específico de task #12?
# Reviso task #12: tiene 2 tags ("backend", "urgent")
# Reviso task #7 (pending, high, 1 tag): no se duplica
# Reviso task #3 (pending, high, 3 tags): aparece 3 veces ← ¡Patrón!
Conclusión del aislamiento: Las tareas se duplican cuando tienen múltiples tags Y se usan filtros combinados. El número de duplicados = número de tags.
Paso 3: DIAGNOSTICAR
Ahora le paso todo a Claude Code:
Mi endpoint GET /api/tasks devuelve resultados duplicados.
He aislado el bug:
- Solo ocurre cuando uso 2+ filtros (status + priority)
- Las tareas se duplican N veces donde N = número de tags que tiene la tarea
- Con un solo filtro no hay duplicados
- Sin filtros no hay duplicados
Esto me sugiere un problema con JOINs en la query.
Código del servicio:
"""python
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import and_
from app.models import Task, Tag
from typing import Optional
class TaskService:
def __init__(self, db: Session):
self.db = db
def get_filtered(
self,
status: Optional[str] = None,
priority: Optional[str] = None
) -> list[Task]:
query = self.db.query(Task).join(Task.tags)
if status:
query = query.filter(Task.status == status)
if priority:
query = query.filter(Task.priority == priority)
return query.all()
"""
Modelos:
"""python
from sqlalchemy import Column, Integer, String, Table, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
task_tags = Table(
'task_tags', Base.metadata,
Column('task_id', Integer, ForeignKey('tasks.id')),
Column('tag_id', Integer, ForeignKey('tags.id'))
)
class Task(Base):
__tablename__ = 'tasks'
id = Column(Integer, primary_key=True)
title = Column(String)
status = Column(String)
priority = Column(String)
tags = relationship('Tag', secondary=task_tags, backref='tasks')
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, primary_key=True)
name = Column(String)
"""
¿Por qué el JOIN con tags causa duplicados cuando hay filtros?
Diagnóstico de Claude Code
El problema es el
JOINcon la tabla de tags. Cuando hacesquery(Task).join(Task.tags), SQLAlchemy genera un INNER JOIN con la tabla intermediatask_tags. Si una tarea tiene 3 tags, el JOIN produce 3 filas para esa tarea (una por cada relación entask_tags).Sin filtros, SQLAlchemy puede deduplicar internamente. Con filtros, la deduplicación no funciona correctamente y obtienes las filas duplicadas.
Fix: Usa
.distinct()o cambia el JOIN a un subquery/exists approach.
Verificación del diagnóstico
- ✅ Tiene sentido: un JOIN con una tabla many-to-many produce duplicados
- ✅ Explica por qué N tags = N duplicados
- ✅ Explica por qué pasa con filtros (la query materializa las filas del JOIN)
Paso 4: FIX
def get_filtered(
self,
status: Optional[str] = None,
priority: Optional[str] = None
) -> list[Task]:
query = self.db.query(Task)
if status:
query = query.filter(Task.status == status)
if priority:
query = query.filter(Task.priority == priority)
return query.options(joinedload(Task.tags)).distinct().all()
Cambios:
- Quité el
.join(Task.tags)que causaba duplicados - Usé
.options(joinedload(Task.tags))para cargar tags sin JOIN en el query principal - Agregué
.distinct()como seguridad extra
Paso 5: VERIFICAR
# El caso que fallaba
curl "http://localhost:8000/api/tasks?status=pending&priority=high"
# ✅ Sin duplicados, task #12 aparece 1 vez
# Filtro individual
curl "http://localhost:8000/api/tasks?status=pending"
# ✅ Sin duplicados
# Sin filtros
curl "http://localhost:8000/api/tasks"
# ✅ Sin duplicados, todos los tasks presentes
# Verificar que los tags se siguen cargando
curl "http://localhost:8000/api/tasks/12"
# ✅ Task #12 muestra sus 2 tags correctamente
# Edge case: tarea sin tags
curl "http://localhost:8000/api/tasks/20" # tarea sin tags
# ✅ Funciona, tags: []
Print Debugging vs Logging vs Debugger
No todas las herramientas de debugging son iguales. Cada una tiene su momento:
Print debugging
def calculate_discount(price, user_tier, coupon_code):
print(f"DEBUG: price={price}, tier={user_tier}, coupon={coupon_code}")
discount = get_base_discount(user_tier)
print(f"DEBUG: base_discount={discount}")
if coupon_code:
coupon_discount = validate_coupon(coupon_code)
print(f"DEBUG: coupon_discount={coupon_discount}")
discount += coupon_discount
final_price = price * (1 - discount)
print(f"DEBUG: final_price={final_price}")
return final_price
Cuándo usar: Bugs simples donde necesitas ver valores en 2-3 puntos. Rápido de agregar, rápido de quitar.
Cuándo NO usar: Cuando tienes más de 5 prints — es hora de usar logging o debugger.
Logging
import logging
logger = logging.getLogger(__name__)
def calculate_discount(price: float, user_tier: str, coupon_code: str | None) -> float:
logger.debug(f"calculate_discount called: price={price}, tier={user_tier}, coupon={coupon_code}")
discount = get_base_discount(user_tier)
logger.debug(f"Base discount for tier '{user_tier}': {discount}")
if coupon_code:
coupon_discount = validate_coupon(coupon_code)
logger.debug(f"Coupon '{coupon_code}' discount: {coupon_discount}")
discount += coupon_discount
final_price = price * (1 - discount)
logger.info(f"Discount applied: {discount*100}%, final_price={final_price}")
return final_price
Cuándo usar: Investigación más compleja. Puedes dejar el logging en el código (a nivel DEBUG) para futuros bugs.
Ventaja sobre prints: Niveles de severidad, timestamps, se puede configurar sin cambiar código.
Debugger (pdb)
def calculate_discount(price, user_tier, coupon_code):
import pdb; pdb.set_trace()
discount = get_base_discount(user_tier)
if coupon_code:
coupon_discount = validate_coupon(coupon_code)
discount += coupon_discount
final_price = price * (1 - discount)
return final_price
Cuándo usar: Bugs complejos donde necesitas inspeccionar el estado de múltiples variables, navegar el call stack, o ejecutar código arbitrario en el contexto del error.
Cuándo NO usar: Bugs en producción (pdb pausa la ejecución), bugs de concurrencia (pdb altera el timing).
Cuándo usar cada herramienta
| Situación | Herramienta |
|---|---|
| "Solo necesito ver un valor" | print() |
| "Necesito ver el flujo completo" | logging |
| "Necesito explorar el estado" | pdb |
| "Necesito interpretar un error" | Claude Code + logs |
| "El bug es de timing/concurrencia" | logging con timestamps |
| "El bug es de performance" | profiler (cProfile, py-spy) |
La Regla de los 30 Minutos
Si llevas 30 minutos en un paso sin avanzar, cambia de enfoque:
- ✅ 30 min en Reproducir: Agrega más logging y espera a que ocurra de nuevo
- ✅ 30 min en Aislar: Pide ayuda a Claude Code para sugerir variables a probar
- ✅ 30 min en Diagnosticar: Abre pdb y ejecuta paso a paso
- ✅ 30 min en Fix: Quizás tu diagnóstico está incompleto — vuelve al paso 3
- ✅ 30 min en Verificar: Si el fix no funciona en edge cases, el fix es incorrecto — vuelve al paso 4
La trampa más común es quedarse atascado en Diagnosticar. Si Claude Code no te da un buen diagnóstico y tú no lo ves claro, agrega más logging, reproduce de nuevo, y vuelve con más datos.
Conexión con Proyecto
Cómo aplica al proyecto integrador (Módulo 8)
En el proyecto integrador tendrás bugs que requieren el proceso completo. No vas a poder simplemente "pegar el error en Claude Code" — vas a necesitar:
- Reproducir cada bug con inputs específicos
- Aislar cuál input o condición causa el error
- Diagnosticar con Claude Code (pasándole logs + código + contexto)
- Aplicar el fix correcto (no un parche)
- Verificar que no rompes nada
La documentación del proceso (qué hiciste en cada paso) es parte de la entrega del proyecto.
Troubleshooting
Problema 1: "No puedo reproducir el bug"
Causa: El bug depende de condiciones que no estás replicando: datos específicos en la DB, estado de sesión, timing. Solución: Agrega logging exhaustivo en la función donde ocurre el error. Incluye todos los inputs y el estado relevante. Espera a que ocurra de nuevo y usa los logs para entender las condiciones exactas.
Problema 2: "Aislar toma demasiado tiempo con inputs complejos"
Causa: El input tiene muchos campos y probar cada combinación es exponencial. Solución: Usa búsqueda binaria: divide los campos en dos mitades, prueba cada mitad. El grupo que falla se divide de nuevo. Para un input de 10 campos, esto reduce de 10 pruebas lineales a ~4 pruebas logarítmicas.
Problema 3: "Mi fix funciona pero rompe otra cosa"
Causa: El fix cambió un comportamiento que otro componente dependía. Solución: Antes de aplicar un fix, busca en el código otros usos de la función/clase que modificas. Pregúntale a Claude Code: "Si cambio [X] a [Y], ¿qué otros componentes podrían verse afectados?"
Problema 4: "El diagnóstico de Claude Code no aplica a mi caso"
Causa: Claude Code no tiene suficiente contexto sobre tu aplicación específica. Solución: Incluye más código relevante en tu prompt. No solo la función que falla — incluye las funciones que la llaman y los modelos que usa. Incluye también qué resultado ESPERABAS vs qué obtuviste.
Ejercicios
Ejercicio 1: Identificar los pasos (Fácil)
Para cada acción, indica a qué paso del proceso pertenece (Reproducir, Aislar, Diagnosticar, Fix, Verificar):
- "Ejecuté curl con los mismos parámetros 3 veces y siempre falla"
- "Cambié
dict[key]pordict.get(key, default_value)" - "Quité campos del request uno por uno hasta encontrar cuál causa el error"
- "Probé con el input original que fallaba y ahora devuelve 200"
- "Le pasé el stack trace a Claude Code y me dijo que es un KeyError porque el diccionario no tiene esa clave"
- "Probé con una lista vacía para ver si el fix maneja ese edge case"
Ver solución
- Reproducir — Confirma que el error es consistente
- Fix — Aplica un cambio en el código
- Aislar — Reduce el input al mínimo que causa el error
- Verificar — Confirma que el fix resuelve el problema original
- Diagnosticar — Usa Claude Code para entender la causa
- Verificar — Prueba edge cases para confirmar que el fix es completo
Ejercicio 2: Diseñar el proceso de aislamiento (Medio)
Tu endpoint POST /api/orders devuelve 500 con este body:
{
"customer_id": 42,
"items": [
{"product_id": 1, "quantity": 2, "price": 29.99},
{"product_id": 5, "quantity": 1, "price": 0},
{"product_id": 3, "quantity": -1, "price": 15.50}
],
"shipping_address": {
"street": "123 Main St",
"city": "Springfield",
"zip": "62701"
},
"coupon_code": "SAVE20",
"notes": ""
}
Describe los pasos de aislamiento que seguirías para encontrar qué parte del input causa el error.
Ver solución
Estrategia: Dividir y conquistar
Ronda 1: ¿Es un campo de primer nivel?
# Solo customer_id + 1 item válido (sin shipping, coupon, notes)
{"customer_id": 42, "items": [{"product_id": 1, "quantity": 2, "price": 29.99}]}
# ¿Falla? Si no, el bug está en shipping_address, coupon_code, o notes
# Solo customer_id + items completos
{"customer_id": 42, "items": [...todos los items...]}
# ¿Falla? Si sí, el bug está en los items
Ronda 2: Si el bug está en items, ¿cuál item?
# Solo item 1
{"customer_id": 42, "items": [{"product_id": 1, "quantity": 2, "price": 29.99}]}
# ¿Falla? Probablemente no (valores normales)
# Solo item 2
{"customer_id": 42, "items": [{"product_id": 5, "quantity": 1, "price": 0}]}
# ¿Falla? Posiblemente (price=0 podría causar división por cero en cálculo de descuento)
# Solo item 3
{"customer_id": 42, "items": [{"product_id": 3, "quantity": -1, "price": 15.50}]}
# ¿Falla? Posiblemente (quantity negativa)
Ronda 3: Si es item 2, ¿es el product_id, quantity, o price?
# price=0 con otros valores normales
{"product_id": 5, "quantity": 1, "price": 0}
# vs
{"product_id": 5, "quantity": 1, "price": 1}
# Si price=0 falla y price=1 funciona → bug es price=0
Ronda 4: Si es item 3, ¿es quantity negativa?
{"product_id": 3, "quantity": -1, "price": 15.50}
# vs
{"product_id": 3, "quantity": 1, "price": 15.50}
# Si quantity=-1 falla y quantity=1 funciona → bug es quantity negativa
Posibles bugs encontrados:
price=0→ probable ZeroDivisionError en cálculo de descuento porcentualquantity=-1→ probable valor negativo en cálculo de total (o error de validación)coupon_code="SAVE20"+price=0→ combinación problemáticanotes=""→ podría causar error si el código espera None en lugar de string vacío
Ejercicio 3: Escribir el prompt de diagnóstico (Medio)
Después de aislar el bug del ejercicio anterior, determines que el error es price=0. El stack trace es:
Traceback (most recent call last):
File "/app/services/order_service.py", line 55, in calculate_total
discount_per_unit = (original_price - item.price) / original_price * 100
ZeroDivisionError: float division by zero
Escribe el prompt que le darías a Claude Code para diagnosticar y obtener un fix.
Ver solución
Mi endpoint POST /api/orders falla con ZeroDivisionError cuando
un item tiene price=0. El error está en el cálculo de descuento
por unidad.
Stack trace:
"""
Traceback (most recent call last):
File "/app/services/order_service.py", line 55, in calculate_total
discount_per_unit = (original_price - item.price) / original_price * 100
ZeroDivisionError: float division by zero
"""
El cálculo intenta determinar qué porcentaje de descuento tiene
cada item comparando item.price con el precio original del producto.
Contexto:
- original_price viene de la tabla products (precio de catálogo)
- item.price viene del request (precio que el cliente está pagando)
- Si item.price = 0, es un item gratuito (promoción)
- Si original_price = 0, es un error de datos (no debería existir un producto gratis en catálogo)
Preguntas:
1. ¿El ZeroDivisionError es porque original_price=0 o porque
estoy calculando mal el descuento?
2. ¿Cuál es la forma correcta de manejar ambos casos
(item gratis por promoción vs producto con precio 0 en catálogo)?
3. ¿Debería validar price > 0 en el schema Pydantic del request?
Por qué este prompt es efectivo:
- ✅ Incluye el stack trace exacto
- ✅ Explica el contexto de negocio (qué significan los valores)
- ✅ Distingue dos casos: item gratis (válido) vs producto sin precio (error)
- ✅ Hace preguntas específicas en lugar de solo "¿qué hago?"
Ejercicio 4: Proceso completo en papel (Difícil)
Lee este escenario y diseña los 5 pasos del proceso sin ejecutar nada. Documenta qué harías en cada paso.
El bug: Tu endpoint GET /api/users/{id}/tasks devuelve tareas de OTROS usuarios mezcladas con las del usuario solicitado. El reporte dice "a veces devuelve tareas que no son mías."
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from typing import Optional
from app.database import get_db
from app.models import Task
app = FastAPI()
tasks_cache = {}
@app.get("/api/users/{user_id}/tasks")
async def get_user_tasks(
user_id: int,
status: Optional[str] = None,
db: Session = Depends(get_db)
):
cache_key = f"user_tasks_{status}"
if cache_key in tasks_cache:
return tasks_cache[cache_key]
query = db.query(Task).filter(Task.owner_id == user_id)
if status:
query = query.filter(Task.status == status)
tasks = query.all()
tasks_cache[cache_key] = [t.to_dict() for t in tasks]
return tasks_cache[cache_key]
Ver solución
Paso 1: REPRODUCIR
Haría dos requests con diferentes user_ids:
1. GET /api/users/1/tasks?status=pending → guarda resultado A
2. GET /api/users/2/tasks?status=pending → ¿devuelve resultado A?
Si el segundo request devuelve las tareas del usuario 1,
el bug es reproducible.
Paso 2: AISLAR
¿Es el user_id o el status filter?
- GET /api/users/1/tasks (sin status) → resultado correcto?
- GET /api/users/2/tasks (sin status) → resultado correcto?
- GET /api/users/1/tasks?status=pending → resultado correcto?
- GET /api/users/2/tasks?status=pending → ¿resultado de user 1?
Hipótesis: el bug aparece cuando uso el mismo filtro de status
para diferentes usuarios.
Paso 3: DIAGNOSTICAR
Mirando el código, el bug es evidente:
cache_key = f"user_tasks_{status}" # ← NO incluye user_id
La cache key es "user_tasks_pending" para TODOS los usuarios.
Cuando el usuario 1 hace el request, se cachea. Cuando el usuario 2
hace el mismo request, se devuelve la cache del usuario 1.
Paso 4: FIX
cache_key = f"user_tasks_{user_id}_{status}"
Pero además debería considerar:
- ¿La cache se invalida cuando se crean/modifican tareas?
- ¿El tamaño de la cache crece indefinidamente?
- ¿Debería usar un TTL?
Fix más robusto:
from functools import lru_cache
from datetime import datetime, timedelta
CACHE_TTL = timedelta(minutes=5)
tasks_cache = {}
def get_cached(key):
if key in tasks_cache:
value, timestamp = tasks_cache[key]
if datetime.utcnow() - timestamp < CACHE_TTL:
return value
del tasks_cache[key]
return None
def set_cached(key, value):
tasks_cache[key] = (value, datetime.utcnow())
@app.get("/api/users/{user_id}/tasks")
async def get_user_tasks(
user_id: int,
status: Optional[str] = None,
db: Session = Depends(get_db)
):
cache_key = f"user_tasks_{user_id}_{status}"
cached = get_cached(cache_key)
if cached is not None:
return cached
query = db.query(Task).filter(Task.owner_id == user_id)
if status:
query = query.filter(Task.status == status)
result = [t.to_dict() for t in query.all()]
set_cached(cache_key, result)
return result
Paso 5: VERIFICAR
□ GET /api/users/1/tasks?status=pending → tareas de user 1 ✅
□ GET /api/users/2/tasks?status=pending → tareas de user 2 ✅
□ GET /api/users/1/tasks → tareas de user 1 (sin filtro) ✅
□ GET /api/users/2/tasks → tareas de user 2 (sin filtro) ✅
□ Mismo user, diferente status → resultados diferentes ✅
□ Después de crear una tarea para user 1:
→ GET /api/users/1/tasks puede no mostrarla hasta que expire la cache
→ ¿Es aceptable? Si no, necesito invalidación de cache
Resumen
En esta cápsula aprendiste:
- El proceso de 5 pasos (reproducir → aislar → diagnosticar → fix → verificar) es el backbone del debugging profesional
- Reproducir es la base: si no puedes reproducir el bug, no puedes confirmar el fix
- Aislar reduce el problema al input mínimo — esencial para diagnósticos precisos
- Diagnosticar es donde Claude Code más ayuda, pero necesita buenos datos (logs + código + contexto)
- Fix debe ser de raíz, no de parche — arregla la causa, no el síntoma
- Verificar incluye el caso original + edge cases + regresión
- La regla de los 30 minutos: si estás atascado, cambia de enfoque o vuelve al paso anterior
- Print vs logging vs debugger: cada herramienta tiene su momento
Próxima cápsula: Cuando Claude Code No Ayuda — las limitaciones reales y cuándo usar herramientas manuales.
Recursos Adicionales
- Debugging: The 9 Indispensable Rules - Libro clásico sobre principios de debugging
- Python pdb — The Python Debugger - Documentación oficial de pdb
- Real Python — Python Debugging with pdb - Tutorial práctico de pdb
- SQLAlchemy — FAQ: Sessions and Queries - Preguntas frecuentes sobre queries (útil para bugs de duplicados)
- FastAPI — Testing - Cómo escribir tests para verificar fixes
Debugging & Code Review with Claude Code — Módulo 6, Cápsula 04 Claude Code Agentic Development Path — Guía #6 de 11