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.pyllamó aserver.pyque llamó ahandlers.pyque llamó aservices.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:
-
Error:
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'sum()está intentando sumar unintconNone- Algún task tiene
priority_score = None
-
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_scorecomoNone,sum()falla
- El generator expression itera sobre tasks y accede a
-
Hipótesis propia: Hay al menos una tarea con
priority_scoreno 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_scorees nullable ysum()no manejaNone - ⚠️ 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:
- Primer error (arriba):
NoResultFound— la query no encontró al usuario - Segundo error (abajo): Dentro del
exceptque maneja elNoResultFound, el código intenta usarcache.get()perocacheesNone
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:
- Ejecutar los endpoints y capturar stack traces
- Interpretar los stack traces (primero tú, después con Claude Code)
- Verificar que el diagnóstico tiene sentido contra el código
- 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_idse pasa comointpero las claves del diccionario sonstr(o viceversa) - El diccionario
self.usersno 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 ae.response.text - Pero
ConnectTimeoutno tiene atributoresponse(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_dateesstren Pydantic perocreated_atesDateTimeen 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:
-
Race condition: Otro request redujo el stock entre el query y el cálculo. Si dos requests de
quantity=5llegan al mismo tiempo, ambos leenstock=10, pero cuando el segundo hace commit, el stock ya es 5 y debería ser 0, no -5. -
Tipo de dato incorrecto:
quantitypodría ser negativo (el usuario envió-5, y10 - (-5) = 15... espera, eso daría positivo). Oquantitypodría ser un string que se convirtió mal. -
El bug más probable: La función se llama con
quantitynegativo 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.stockes 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
- Python — Built-in Exceptions - Referencia oficial de todas las excepciones de Python
- Real Python — Understanding Tracebacks - Guía detallada para entender stack traces
- FastAPI — Handling Errors - Manejo de errores en FastAPI
- SQLAlchemy — Exceptions - Excepciones comunes de SQLAlchemy
- Pydantic — Error Handling - Validación y errores en Pydantic v2
- 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