Módulo 6: Debugging con Claude Code

Runtime Errors y Stack Traces

Runtime Errors y Stack Traces

Descripción de la cápsula

Un stack trace es el mapa que Python te da cuando algo sale mal en runtime. Te muestra exactamente qué funciones se llamaron, en qué orden, y en qué línea explotó todo. Pero si nunca te enseñaron a leerlo, un stack trace de 15 líneas se ve como ruido. Y si le pegas el stack trace a Claude Code sin entenderlo tú mismo, no puedes evaluar si el diagnóstico que te da es correcto.

En esta cápsula vas a aprender a leer stack traces de Python — de abajo hacia arriba, identificando la causa inmediata y rastreando la cadena de llamadas. Después aprenderás a usar Claude Code como asistente de interpretación: no para que te diga qué hacer, sino para que te ayude a entender qué pasó. Y lo más importante: aprenderás los errores de runtime más comunes en Python y qué significan, para que puedas diagnosticar muchos bugs sin necesidad de AI.


Anatomía de un Stack Trace de Python

Cada stack trace de Python tiene la misma estructura. Apréndela una vez y podrás leer cualquier stack trace:

Traceback (most recent call last):          ← Encabezado: siempre igual
  File "/app/main.py", line 12, in <module> ← Frame más antiguo (donde empezó)
    app.run()
  File "/app/server.py", line 45, in run    ← Frame intermedio
    handle_request(request)
  File "/app/handlers.py", line 78, in handle_request  ← Frame intermedio
    result = process_data(request.body)
  File "/app/services.py", line 23, in process_data    ← Frame más reciente
    value = data['key']                     ← Línea exacta que falló
KeyError: 'key'                             ← El error: tipo + mensaje

Las 3 partes que importan

1. El error (última línea): KeyError: 'key'

  • El tipo de error (KeyError) te dice la categoría
  • El mensaje ('key') te da el detalle específico

2. El frame más reciente (penúltima sección):

  File "/app/services.py", line 23, in process_data
    value = data['key']
  • Dónde ocurrió el error exactamente
  • Qué línea de código causó el error

3. La cadena de llamadas (todos los frames):

  • De arriba hacia abajo: cómo llegamos a ese punto
  • main.py llamó a server.py que llamó a handlers.py que llamó a services.py

Regla de oro: Lee de abajo hacia arriba

La información más importante está al final del stack trace. Empieza por el error, sube al frame más reciente, y solo si necesitas más contexto, lee los frames superiores.


Las 10 Excepciones Más Comunes de Python

Antes de pasarle un error a Claude Code, intenta diagnosticarlo tú. Estos 10 errores representan el 80% de lo que encontrarás:

1. KeyError

data = {"name": "Ana", "email": "ana@test.com"}
user_id = data["user_id"]  # KeyError: 'user_id'

Qué significa: Buscas una clave que no existe en un diccionario. Fix común: Usa .get() con un valor por defecto, o verifica antes de acceder.

user_id = data.get("user_id")
# o
if "user_id" in data:
    user_id = data["user_id"]

2. TypeError

def calculate_total(price, quantity):
    return price * quantity

result = calculate_total("100", 3)  # "100100100" — no es un error, pero es un bug
result = calculate_total(None, 3)   # TypeError: unsupported operand type(s)

Qué significa: Una operación recibe un tipo de dato que no esperaba. Fix común: Validar tipos en la entrada o hacer conversión explícita.

3. AttributeError

from typing import Optional

user: Optional[dict] = None
email = user.get("email")  # AttributeError: 'NoneType' object has no attribute 'get'

Qué significa: Intentas acceder a un atributo o método de un objeto que no lo tiene — frecuentemente porque el objeto es None. Fix común: Verificar que el objeto no sea None antes de acceder.

4. ValueError

age = int("twenty")  # ValueError: invalid literal for int() with base 10: 'twenty'

Qué significa: El valor es del tipo correcto pero tiene un contenido inválido. Fix común: Validar el contenido antes de la conversión, o usar try/except.

5. IndexError

items = [1, 2, 3]
last = items[5]  # IndexError: list index out of range

Qué significa: Accedes a un índice que no existe en una lista. Fix común: Verificar len(items) antes de acceder, o usar slicing seguro.

6. ImportError / ModuleNotFoundError

from sklearn.metrics import roc_auc_multiclass  # ModuleNotFoundError

Qué significa: El módulo o función que importas no existe — frecuente en hallucinations de AI. Fix común: Verificar que el paquete está instalado y que la función existe.

7. ZeroDivisionError

completion_rate = completed / total * 100  # ZeroDivisionError si total == 0

Qué significa: División por cero. Fix común: Verificar el divisor antes de dividir.

8. FileNotFoundError

with open("/app/config/settings.json") as f:  # FileNotFoundError
    config = json.load(f)

Qué significa: El archivo o directorio no existe. Fix común: Verificar existencia antes de abrir, o usar Path.exists().

9. ConnectionError / TimeoutError

import httpx

response = httpx.get("https://api.external.com/data", timeout=5)
# httpx.ConnectTimeout: timed out

Qué significa: No se pudo conectar a un servicio externo. Fix común: Retry con backoff, timeout apropiado, circuit breaker.

10. ValidationError (Pydantic)

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    email: EmailStr
    age: int

user = User(email="not-an-email", age="young")
# ValidationError: 2 validation errors for User

Qué significa: Los datos no cumplen con el schema de Pydantic. Fix común: Validar datos antes de crear el modelo, o manejar el error.


Cómo Pasar un Stack Trace a Claude Code

Lo básico: Stack trace completo + contexto

Mi aplicación FastAPI devuelve un error 500 cuando intento crear una 
tarea con prioridad "urgent". Funciona con "high", "medium", y "low".

Stack trace:
"""
Traceback (most recent call last):
  File "/app/routers/tasks.py", line 34, in create_task
    validated = TaskCreate(**request_data)
  File "/app/models/task.py", line 18, in __init__
    super().__init__(**data)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for TaskCreate
priority
  value is not a valid enumeration member; permitted: 'low', 'medium', 'high' 
  (type=type_error.enum; enum_values=['low', 'medium', 'high'])
"""

El modelo Task:
"""python
class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
"""

Por qué incluir el código relevante

Nota que en el ejemplo anterior incluimos el modelo Priority. Sin eso, Claude Code puede diagnosticar que "urgent" no está en el enum, pero no puede sugerirte el fix exacto. Con el código, puede decirte: "Agrega URGENT = 'urgent' al enum Priority" — o, si no debería ser un valor válido: "Valida el input antes de crear el modelo y devuelve un 400 con los valores permitidos."

Template: Stack trace + código relevante

[Descripción breve del error y cuándo ocurre]

Stack trace:
"""
[stack trace completo]
"""

Código relevante ([nombre del archivo]):
"""python
[el código del archivo/función donde ocurre el error]
"""

Contexto:
- [Qué estabas haciendo cuando ocurrió]
- [Si funcionaba antes, qué cambió]
- [Si es intermitente, cuándo sí funciona]

Ejemplo Completo: TypeError en un Endpoint

El error

Tu endpoint GET /api/tasks/stats devuelve un error 500. Los logs muestran:

2026-03-13 16:30:00 INFO     GET /api/tasks/stats
Traceback (most recent call last):
  File "/app/routers/tasks.py", line 67, in get_stats
    stats = task_service.calculate_stats(tasks)
  File "/app/services/task_service.py", line 112, in calculate_stats
    avg_priority = sum(t.priority_score for t in tasks) / len(tasks)
  File "/app/services/task_service.py", line 112, in <genexpr>
    avg_priority = sum(t.priority_score for t in tasks) / len(tasks)
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

Paso 1: Lee el stack trace tú mismo

De abajo hacia arriba:

  1. Error: TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

    • sum() está intentando sumar un int con None
    • Algún task tiene priority_score = None
  2. Frame: avg_priority = sum(t.priority_score for t in tasks) / len(tasks)

    • El generator expression itera sobre tasks y accede a priority_score
    • Si cualquier task tiene priority_score como None, sum() falla
  3. Hipótesis propia: Hay al menos una tarea con priority_score no asignado (None).

Paso 2: Confirma o expande con Claude Code

Mi endpoint de stats falla con este TypeError. Creo que el problema 
es que alguna tarea tiene priority_score=None, pero quiero confirmar 
y saber la mejor forma de manejarlo.

Stack trace:
"""
[el stack trace de arriba]
"""

El modelo Task relevante:
"""python
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class Task(Base):
    __tablename__ = "tasks"
    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    status = Column(String, nullable=False, default="pending")
    priority_score = Column(Integer, nullable=True)  # puede ser None
    created_at = Column(DateTime)
"""

Paso 3: Evaluación del diagnóstico de Claude Code

Claude Code confirmará tu hipótesis y probablemente sugerirá:

avg_priority = sum(
    t.priority_score for t in tasks if t.priority_score is not None
) / max(
    sum(1 for t in tasks if t.priority_score is not None), 1
)

Tu evaluación:

  • ✅ El diagnóstico es correcto: priority_score es nullable y sum() no maneja None
  • ⚠️ El fix sugerido es funcional pero tiene un problema: si TODOS los priority_score son None, devuelve 0 (por el max(..., 1)) sin indicar que no hay datos
  • Mejora: agregar un check explícito y devolver un indicador de "sin datos"

Paso 4: Tu fix mejorado

from typing import Optional

def calculate_stats(self, tasks: list) -> dict:
    scored_tasks = [t for t in tasks if t.priority_score is not None]

    if not scored_tasks:
        avg_priority = None
    else:
        avg_priority = sum(t.priority_score for t in scored_tasks) / len(scored_tasks)

    return {
        "total": len(tasks),
        "avg_priority": avg_priority,
        "tasks_without_score": len(tasks) - len(scored_tasks)
    }

Stack Traces Encadenados: Excepciones que Causan Excepciones

Python 3 muestra cadenas de excepciones con "During handling of the above exception, another exception occurred". Estos son los más confusos pero también los más comunes en aplicaciones FastAPI:

Traceback (most recent call last):
  File "/app/services/user_service.py", line 25, in get_user
    user = db.query(User).filter(User.id == user_id).one()
sqlalchemy.exc.NoResultFound: No row was found for one()

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/routers/users.py", line 15, in get_user_endpoint
    user = user_service.get_user(user_id)
  File "/app/services/user_service.py", line 28, in get_user
    raise ValueError(f"User {user_id} not found in cache: {cache.get(user_id)}")
AttributeError: 'NoneType' object has no attribute 'get'

Cómo leerlo

Lee de abajo hacia arriba, pero entiende las dos partes:

  1. Primer error (arriba): NoResultFound — la query no encontró al usuario
  2. Segundo error (abajo): Dentro del except que maneja el NoResultFound, el código intenta usar cache.get() pero cache es None

El error que ves es el segundo (AttributeError), pero la causa raíz es el primero (NoResultFound) combinado con un handler de errores buggy.

Cómo pasarlo a Claude Code

Mi endpoint devuelve 500 con un stack trace encadenado. El error 
original es que no encuentra un usuario en la DB, pero el handler 
del error también falla.

Stack trace completo:
"""
[el stack trace de arriba]
"""

Preguntas:
1. ¿Por qué el handler del error (except block) también falla?
2. ¿Cuál es el fix correcto — arreglar el handler, o evitar 
   que llegue al handler?

Errores de Runtime Específicos de FastAPI

1. RequestValidationError

INFO:     127.0.0.1:54372 - "POST /api/tasks HTTP/1.1" 422
{
  "detail": [
    {
      "loc": ["body", "priority"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Qué significa: El request no cumple con el schema de Pydantic. FastAPI devuelve 422 automáticamente. Qué investigar: ¿El cliente está enviando los campos correctos? ¿El schema de Pydantic cambió?

2. HTTPException vs excepciones no manejadas

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/api/tasks/{task_id}")
async def get_task(task_id: int):
    task = find_task(task_id)

    # Esto devuelve 404 con mensaje limpio:
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")

    # Esto devuelve 500 con stack trace:
    return {"title": task.name}  # AttributeError si task no tiene .name

La diferencia: HTTPException es un error controlado que tú manejas. Un AttributeError es un error no controlado que FastAPI convierte en 500.

3. Errores de dependencias (Depends)

Traceback (most recent call last):
  File "/app/routers/tasks.py", line 8, in create_task
    async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
  File "/app/database.py", line 15, in get_db
    db = SessionLocal()
  File "sqlalchemy/orm/session.py", line 234, in __init__
    ...
sqlalchemy.exc.OperationalError: could not connect to server: Connection refused

Qué significa: La dependencia (en este caso get_db) falla antes de que tu función se ejecute. Qué investigar: ¿La base de datos está corriendo? ¿Las credenciales son correctas?

4. Errores async/await

from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/api/external-data")
async def get_external():
    # Error: httpx.get es síncrono dentro de una función async
    response = httpx.get("https://api.external.com/data")
    return response.json()

Esto funciona pero bloquea el event loop. El error correcto usa el cliente async:

from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/api/external-data")
async def get_external():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.external.com/data")
    return response.json()

Verificando el Diagnóstico de Claude Code: El Checklist

Después de que Claude Code te dé un diagnóstico basado en un stack trace, verifica con este checklist:

□ ¿El diagnóstico es consistente con el tipo de error?
  (Si dice "KeyError" pero el trace dice "TypeError", algo está mal)

□ ¿La línea de código que menciona Claude Code coincide 
  con la del stack trace?

□ ¿El fix propuesto maneja el caso edge que causó el error?

□ ¿El fix podría causar un error nuevo? 
  (e.g., un try/except que silencia un error importante)

□ ¿Hay otros lugares en el código con el mismo patrón 
  que podrían tener el mismo bug?

Conexión con Proyecto

Cómo aplica al proyecto integrador (Módulo 8)

El proyecto integrador incluye runtime bugs — errores que solo se manifiestan al ejecutar la aplicación con ciertos inputs. Necesitarás:

  1. Ejecutar los endpoints y capturar stack traces
  2. Interpretar los stack traces (primero tú, después con Claude Code)
  3. Verificar que el diagnóstico tiene sentido contra el código
  4. Aplicar el fix y confirmar que no introduces nuevos errores

La habilidad de leer un stack trace sin ayuda de AI te hace más rápido. La habilidad de usar Claude Code para stack traces complejos (encadenados, con múltiples capas de abstracción) te hace más efectivo.


Troubleshooting

Problema 1: "El stack trace tiene 50 líneas y no sé por dónde empezar"

Causa: Stack traces largos incluyen frames de librerías (FastAPI, SQLAlchemy, Pydantic) que son internos y no son tu código. Solución: Busca los frames que mencionan TUS archivos (/app/, src/). Ignora los frames de librerías a menos que el error esté en cómo usas la librería. Empieza siempre por la última línea (el error) y sube hasta el primer frame que sea tu código.

Problema 2: "Claude Code sugiere un fix pero cambia la lógica"

Causa: Claude Code a veces "arregla" el error cambiando lo que el código hace, no cómo lo hace. Solución: Verifica que el fix mantiene el comportamiento esperado. Un try/except que devuelve un valor por defecto "arregla" el error pero puede ocultar un bug. Pregúntate: "¿El código debería manejar este caso, o este caso no debería llegar aquí?"

Problema 3: "El error solo aparece en producción, no en desarrollo"

Causa: Diferencias de entorno: versiones de librerías, datos de la DB, variables de entorno, timing. Solución: Captura el stack trace completo de producción (con logs). Verifica que las versiones de librerías en requirements.txt son las mismas. Intenta reproducir con los mismos datos (anonimizados).

Problema 4: "No entiendo qué significa el error de la librería"

Causa: Errores como sqlalchemy.exc.IntegrityError o pydantic.error_wrappers.ValidationError no son autoexplicativos. Solución: Pásale el stack trace a Claude Code con el contexto "No entiendo qué significa este error de [librería]. Explícame en términos simples qué lo causa." Claude Code es muy bueno explicando errores de librerías populares.


Ejercicios

Ejercicio 1: Leer un stack trace (Fácil)

Lee este stack trace y responde: (a) ¿Qué tipo de error es? (b) ¿En qué archivo y línea ocurrió? (c) ¿Cuál es la causa probable?

Traceback (most recent call last):
  File "/app/routers/users.py", line 23, in update_user
    user_data = user_service.get_user(user_id)
  File "/app/services/user_service.py", line 45, in get_user
    return self.users[user_id]
KeyError: 42
Ver solución

(a) Tipo de error: KeyError — se busca una clave que no existe en un diccionario.

(b) Archivo y línea: /app/services/user_service.py, línea 45, en la función get_user.

(c) Causa probable: self.users es un diccionario y no contiene la clave 42. Esto puede significar:

  • El usuario con ID 42 no existe en el almacenamiento en memoria
  • El user_id se pasa como int pero las claves del diccionario son str (o viceversa)
  • El diccionario self.users no se pobló correctamente

Fix más robusto:

def get_user(self, user_id: int):
    user = self.users.get(user_id)
    if user is None:
        raise HTTPException(status_code=404, detail=f"User {user_id} not found")
    return user

Ejercicio 2: Diagnosticar un TypeError (Medio)

Este stack trace aparece cuando un usuario intenta crear una tarea. Diagnostica el problema y propón un fix.

Traceback (most recent call last):
  File "/app/routers/tasks.py", line 15, in create_task
    new_task = task_service.create(task_data, current_user)
  File "/app/services/task_service.py", line 30, in create
    task = Task(
        title=data.title,
        owner_id=user.id,
        due_date=data.due_date,
        tags=",".join(data.tags)
    )
  File "/app/services/task_service.py", line 30, in create
    tags=",".join(data.tags)
TypeError: can only join an iterable

Contexto: el modelo Pydantic del request es:

from pydantic import BaseModel
from typing import Optional
from datetime import datetime

class TaskCreate(BaseModel):
    title: str
    due_date: Optional[datetime] = None
    tags: Optional[list[str]] = None
Ver solución

Diagnóstico:

El error es TypeError: can only join an iterable en la línea tags=",".join(data.tags).

data.tags es Optional[list[str]], lo que significa que puede ser None. Cuando el usuario no envía tags en el request, data.tags es None, y ",".join(None) falla porque None no es iterable.

Fix:

def create(self, data: TaskCreate, user) -> Task:
    tags_str = ",".join(data.tags) if data.tags else ""

    task = Task(
        title=data.title,
        owner_id=user.id,
        due_date=data.due_date,
        tags=tags_str
    )
    return task

Nota: Este es un patrón extremadamente común en código AI-generated. Claude Code tiende a generar código que funciona con el "happy path" (todos los campos presentes) pero no maneja los campos opcionales correctamente.

Ejercicio 3: Stack trace encadenado (Difícil)

Interpreta este stack trace encadenado. Identifica: (a) el error original, (b) el error secundario, (c) cuál es el bug real que necesitas arreglar.

Traceback (most recent call last):
  File "/app/services/payment_service.py", line 22, in process_payment
    response = payment_gateway.charge(amount=order.total, card_token=order.card_token)
  File "/app/external/gateway.py", line 55, in charge
    result = self._make_request("POST", "/charges", data=payload)
  File "/app/external/gateway.py", line 30, in _make_request
    resp = httpx.post(url, json=data, timeout=10)
httpx.ConnectTimeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/app/routers/orders.py", line 45, in checkout
    payment_result = payment_service.process_payment(order)
  File "/app/services/payment_service.py", line 28, in process_payment
    logger.error(f"Payment failed for order {order.id}: {e.response.text}")
AttributeError: 'ConnectTimeout' object has no attribute 'response'
Ver solución

(a) Error original: httpx.ConnectTimeout: timed out

  • El servicio de pagos externo no respondió en 10 segundos
  • Esto es un error de red/infraestructura, no de código

(b) Error secundario: AttributeError: 'ConnectTimeout' object has no attribute 'response'

  • En el bloque except, el código intenta acceder a e.response.text
  • Pero ConnectTimeout no tiene atributo response (porque nunca hubo una respuesta HTTP — la conexión ni siquiera se estableció)
  • Este es un bug en el error handler

(c) El bug real que necesitas arreglar:

El error handler asume que todas las excepciones de httpx tienen un .response, pero ConnectTimeout ocurre antes de recibir una respuesta. El fix correcto:

import httpx
import logging

logger = logging.getLogger(__name__)

def process_payment(self, order):
    try:
        response = payment_gateway.charge(
            amount=order.total,
            card_token=order.card_token
        )
        return response
    except httpx.ConnectTimeout:
        logger.error(f"Payment gateway timeout for order {order.id}")
        raise HTTPException(
            status_code=503,
            detail="Payment service temporarily unavailable"
        )
    except httpx.HTTPStatusError as e:
        logger.error(
            f"Payment failed for order {order.id}: {e.response.text}"
        )
        raise HTTPException(
            status_code=502,
            detail="Payment processing failed"
        )
    except httpx.HTTPError as e:
        logger.error(f"Payment error for order {order.id}: {str(e)}")
        raise HTTPException(
            status_code=502,
            detail="Payment service error"
        )

Lecciones:

  • Maneja diferentes tipos de excepción por separado
  • ConnectTimeout → el servicio no está disponible (503)
  • HTTPStatusError → el servicio respondió con error (este sí tiene .response)
  • HTTPError → catch-all para otros errores de red

Ejercicio 4: Prompt para Claude Code (Medio)

Escribe el prompt que le darías a Claude Code para este stack trace. Incluye el contexto necesario para un buen diagnóstico.

Traceback (most recent call last):
  File "/app/routers/tasks.py", line 52, in list_tasks
    tasks = task_service.get_filtered(filters)
  File "/app/services/task_service.py", line 78, in get_filtered
    query = query.filter(Task.created_at >= filters.start_date)
  File "sqlalchemy/sql/type_api.py", line 387, in process
    ...
sqlalchemy.exc.StatementError: (builtins.TypeError) 
    Not a string or datetime object: '2026-03-13'

Contexto: los filtros vienen de query parameters del endpoint.

Ver solución

Buen prompt:

Mi endpoint GET /api/tasks acepta filtros por fecha. Cuando paso 
?start_date=2026-03-13, SQLAlchemy devuelve un error diciendo que 
el valor no es un string ni un datetime object.

Stack trace:
"""
Traceback (most recent call last):
  File "/app/routers/tasks.py", line 52, in list_tasks
    tasks = task_service.get_filtered(filters)
  File "/app/services/task_service.py", line 78, in get_filtered
    query = query.filter(Task.created_at >= filters.start_date)
  File "sqlalchemy/sql/type_api.py", line 387, in process
    ...
sqlalchemy.exc.StatementError: (builtins.TypeError) 
    Not a string or datetime object: '2026-03-13'
"""

Modelo SQLAlchemy:
"""python
class Task(Base):
    __tablename__ = "tasks"
    created_at = Column(DateTime, default=datetime.utcnow)
"""

Schema de filtros:
"""python
class TaskFilters(BaseModel):
    start_date: Optional[str] = None
    end_date: Optional[str] = None
"""

Preguntas:
1. ¿Por qué SQLAlchemy rechaza '2026-03-13' si 
   Task.created_at es DateTime?
2. ¿El problema está en el tipo del filtro (str) vs 
   el tipo de la columna (DateTime)?
3. ¿Cuál es la forma correcta de manejar la conversión?

Por qué funciona:

  • ✅ Describe el escenario (qué endpoint, qué parámetro)
  • ✅ Incluye el stack trace completo
  • ✅ Incluye AMBOS modelos relevantes (SQLAlchemy y Pydantic)
  • ✅ Hace preguntas específicas que guían el diagnóstico
  • ✅ El bug es claro: start_date es str en Pydantic pero created_at es DateTime en SQLAlchemy — necesita conversión

Fix esperado: Cambiar el tipo en Pydantic a Optional[datetime] o convertir el string a datetime antes de filtrar.

Ejercicio 5: Encontrar el bug real (Difícil)

Este stack trace NO dice toda la verdad. Lee el código, compara con el error, y determina si el fix obvio es el fix correcto.

Stack trace:

Traceback (most recent call last):
  File "/app/services/inventory.py", line 45, in update_stock
    new_quantity = current_stock - quantity
  File "/app/services/inventory.py", line 46, in update_stock
    if new_quantity < 0:
  File "/app/services/inventory.py", line 47, in update_stock
    raise ValueError("Insufficient stock")
ValueError: Insufficient stock

Código completo de la función:

from typing import Optional
from app.models import Product
from app.database import get_db
from sqlalchemy.orm import Session

class InventoryService:
    def __init__(self, db: Session):
        self.db = db

    def update_stock(self, product_id: int, quantity: int) -> Product:
        product = self.db.query(Product).get(product_id)
        current_stock = product.stock

        new_quantity = current_stock - quantity
        if new_quantity < 0:
            raise ValueError("Insufficient stock")

        product.stock = new_quantity
        self.db.commit()
        return product

El contexto: El producto tiene stock=10. El usuario pidió quantity=5. Debería funcionar, pero lanza "Insufficient stock". ¿Por qué?

Ver solución

El fix obvio es incorrecto. El stack trace dice ValueError: Insufficient stock, lo que sugiere que new_quantity < 0. Pero 10 - 5 = 5, que no es negativo.

El bug real está en otra parte. Posibles causas que el stack trace no revela:

  1. Race condition: Otro request redujo el stock entre el query y el cálculo. Si dos requests de quantity=5 llegan al mismo tiempo, ambos leen stock=10, pero cuando el segundo hace commit, el stock ya es 5 y debería ser 0, no -5.

  2. Tipo de dato incorrecto: quantity podría ser negativo (el usuario envió -5, y 10 - (-5) = 15... espera, eso daría positivo). O quantity podría ser un string que se convirtió mal.

  3. El bug más probable: La función se llama con quantity negativo para representar "devolver stock" (como una convención), y alguien llamó update_stock(product_id=1, quantity=-15). Entonces: 10 - (-15) = 25, que es positivo... no, eso tampoco falla.

Revisión más cuidadosa: Si stock=10 y quantity=5 realmente lanza el error, el problema podría ser que product.stock no es 10 en el momento de la query. Causas:

  • La DB tiene un valor diferente al esperado
  • Hay un trigger en la DB que modifica el stock
  • Otro proceso modificó el stock entre el query y el check
  • product.stock es None (si la columna permite null)

La lección: Un stack trace te dice DÓNDE falla, pero no siempre te dice POR QUÉ. Para bugs donde los datos no son los que esperas, necesitas agregar logging o usar pdb para inspeccionar los valores en runtime.

Fix de investigación (no el fix final):

def update_stock(self, product_id: int, quantity: int) -> Product:
    product = self.db.query(Product).get(product_id)
    current_stock = product.stock

    import logging
    logger = logging.getLogger(__name__)
    logger.debug(
        f"update_stock: product_id={product_id}, "
        f"current_stock={current_stock} (type={type(current_stock)}), "
        f"quantity={quantity} (type={type(quantity)})"
    )

    new_quantity = current_stock - quantity
    if new_quantity < 0:
        raise ValueError(
            f"Insufficient stock: current={current_stock}, "
            f"requested={quantity}, result={new_quantity}"
        )

    product.stock = new_quantity
    self.db.commit()
    return product

Agregar logging con los valores concretos te dirá exactamente por qué la condición se activa.


Resumen

En esta cápsula aprendiste:

  • Los stack traces de Python se leen de abajo hacia arriba: error → frame reciente → cadena de llamadas
  • Las 10 excepciones más comunes representan el 80% de los errores que encontrarás
  • Para pasar un stack trace a Claude Code, incluye el trace completo + código relevante + contexto
  • Los stack traces encadenados tienen un error original y un error en el handler — identifica ambos
  • Los errores específicos de FastAPI (422, Depends, async) tienen patrones reconocibles
  • Siempre verifica el diagnóstico de Claude Code: ¿es consistente con el error? ¿el fix podría causar un nuevo bug?
  • A veces el stack trace te dice dónde pero no por qué — necesitas agregar logging para investigar

Próxima cápsula: Debugging Sistemático — el proceso completo de reproducir → aislar → diagnosticar → fix → verificar.


Recursos Adicionales

  1. Python — Built-in Exceptions - Referencia oficial de todas las excepciones de Python
  2. Real Python — Understanding Tracebacks - Guía detallada para entender stack traces
  3. FastAPI — Handling Errors - Manejo de errores en FastAPI
  4. SQLAlchemy — Exceptions - Excepciones comunes de SQLAlchemy
  5. Pydantic — Error Handling - Validación y errores en Pydantic v2
  6. httpx — Exceptions - Excepciones del cliente HTTP httpx

Debugging & Code Review with Claude Code — Módulo 6, Cápsula 03 Claude Code Agentic Development Path — Guía #6 de 11