Module 6: Debugging with Claude Code
Runtime Errors and Stack Traces
Runtime Errors and Stack Traces
Capsule overview
A stack trace is the map Python gives you when something goes wrong at runtime. It shows you exactly which functions were called, in what order, and on which line everything blew up. But if you were never taught to read it, a 15-line stack trace looks like noise. And if you paste the stack trace to Claude Code without understanding it yourself, you can't evaluate whether the diagnosis it gives you is correct.
In this capsule you're going to learn to read Python stack traces — from the bottom up, identifying the immediate cause and tracing the chain of calls. Then you'll learn to use Claude Code as an interpretation assistant: not to tell you what to do, but to help you understand what happened. And most importantly: you'll learn the most common runtime errors in Python and what they mean, so you can diagnose many bugs without needing AI.
Anatomy of a Python Stack Trace
Every Python stack trace has the same structure. Learn it once and you'll be able to read any stack trace:
Traceback (most recent call last): ← Header: always the same
File "/app/main.py", line 12, in <module> ← Oldest frame (where it started)
app.run()
File "/app/server.py", line 45, in run ← Intermediate frame
handle_request(request)
File "/app/handlers.py", line 78, in handle_request ← Intermediate frame
result = process_data(request.body)
File "/app/services.py", line 23, in process_data ← Most recent frame
value = data['key'] ← The exact line that failed
KeyError: 'key' ← The error: type + message
The 3 parts that matter
1. The error (last line): KeyError: 'key'
- The error type (
KeyError) tells you the category - The message (
'key') gives you the specific detail
2. The most recent frame (second-to-last section):
File "/app/services.py", line 23, in process_data
value = data['key']
- Where the error occurred exactly
- Which line of code caused the error
3. The chain of calls (all the frames):
- From top to bottom: how we got to that point
main.pycalledserver.pywhich calledhandlers.pywhich calledservices.py
Golden rule: Read from the bottom up
The most important information is at the end of the stack trace. Start with the error, go up to the most recent frame, and only if you need more context, read the upper frames.
The 10 Most Common Python Exceptions
Before passing an error to Claude Code, try to diagnose it yourself. These 10 errors represent 80% of what you'll encounter:
1. KeyError
data = {"name": "Ana", "email": "ana@test.com"}
user_id = data["user_id"] # KeyError: 'user_id'
What it means: You're looking up a key that doesn't exist in a dictionary.
Common fix: Use .get() with a default value, or check before accessing.
user_id = data.get("user_id")
# or
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" — not an error, but a bug
result = calculate_total(None, 3) # TypeError: unsupported operand type(s)
What it means: An operation receives a data type it wasn't expecting. Common fix: Validate types at the input or do an explicit conversion.
3. AttributeError
from typing import Optional
user: Optional[dict] = None
email = user.get("email") # AttributeError: 'NoneType' object has no attribute 'get'
What it means: You're trying to access an attribute or method of an object that doesn't have it — frequently because the object is None.
Common fix: Check that the object isn't None before accessing it.
4. ValueError
age = int("twenty") # ValueError: invalid literal for int() with base 10: 'twenty'
What it means: The value is of the correct type but has invalid content. Common fix: Validate the content before the conversion, or use try/except.
5. IndexError
items = [1, 2, 3]
last = items[5] # IndexError: list index out of range
What it means: You're accessing an index that doesn't exist in a list.
Common fix: Check len(items) before accessing, or use safe slicing.
6. ImportError / ModuleNotFoundError
from sklearn.metrics import roc_auc_multiclass # ModuleNotFoundError
What it means: The module or function you're importing doesn't exist — frequent in AI hallucinations. Common fix: Verify that the package is installed and that the function exists.
7. ZeroDivisionError
completion_rate = completed / total * 100 # ZeroDivisionError if total == 0
What it means: Division by zero. Common fix: Check the divisor before dividing.
8. FileNotFoundError
with open("/app/config/settings.json") as f: # FileNotFoundError
config = json.load(f)
What it means: The file or directory doesn't exist.
Common fix: Check existence before opening, or use Path.exists().
9. ConnectionError / TimeoutError
import httpx
response = httpx.get("https://api.external.com/data", timeout=5)
# httpx.ConnectTimeout: timed out
What it means: A connection to an external service couldn't be made. Common fix: Retry with backoff, an appropriate timeout, a 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
What it means: The data doesn't comply with the Pydantic schema. Common fix: Validate the data before creating the model, or handle the error.
How to Pass a Stack Trace to Claude Code
The basics: Complete stack trace + context
My FastAPI application returns a 500 error when I try to create a
task with priority "urgent". It works with "high", "medium", and "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'])
"""
The Task model:
"""python
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
"""
Why include the relevant code
Note that in the previous example we included the Priority model. Without it, Claude Code can diagnose that "urgent" isn't in the enum, but it can't suggest the exact fix. With the code, it can tell you: "Add URGENT = 'urgent' to the Priority enum" — or, if it shouldn't be a valid value: "Validate the input before creating the model and return a 400 with the allowed values."
Template: Stack trace + relevant code
[Brief description of the error and when it occurs]
Stack trace:
"""
[complete stack trace]
"""
Relevant code ([file name]):
"""python
[the code of the file/function where the error occurs]
"""
Context:
- [What you were doing when it occurred]
- [If it was working before, what changed]
- [If it's intermittent, when it does work]
Complete Example: TypeError in an Endpoint
The error
Your GET /api/tasks/stats endpoint returns a 500 error. The logs show:
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'
Step 1: Read the stack trace yourself
From the bottom up:
-
Error:
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'sum()is trying to add anintwithNone- Some task has
priority_score = None
-
Frame:
avg_priority = sum(t.priority_score for t in tasks) / len(tasks)- The generator expression iterates over tasks and accesses
priority_score - If any task has
priority_scoreasNone,sum()fails
- The generator expression iterates over tasks and accesses
-
Your own hypothesis: There's at least one task with an unassigned
priority_score(None).
Step 2: Confirm or expand with Claude Code
My stats endpoint fails with this TypeError. I think the problem
is that some task has priority_score=None, but I want to confirm
and know the best way to handle it.
Stack trace:
"""
[the stack trace above]
"""
The relevant Task model:
"""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) # can be None
created_at = Column(DateTime)
"""
Step 3: Evaluation of Claude Code's diagnosis
Claude Code will confirm your hypothesis and probably suggest:
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
)
Your evaluation:
- ✅ The diagnosis is correct:
priority_scoreis nullable andsum()doesn't handleNone - ⚠️ The suggested fix is functional but has a problem: if ALL priority_scores are None, it returns 0 (because of the
max(..., 1)) without indicating that there's no data - Improvement: add an explicit check and return a "no data" indicator
Step 4: Your improved fix
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)
}
Chained Stack Traces: Exceptions That Cause Exceptions
Python 3 shows chains of exceptions with "During handling of the above exception, another exception occurred". These are the most confusing but also the most common in FastAPI applications:
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'
How to read it
Read from the bottom up, but understand the two parts:
- First error (top):
NoResultFound— the query didn't find the user - Second error (bottom): Inside the
exceptthat handles theNoResultFound, the code tries to usecache.get()butcacheisNone
The error you see is the second one (AttributeError), but the root cause is the first one (NoResultFound) combined with a buggy error handler.
How to pass it to Claude Code
My endpoint returns 500 with a chained stack trace. The original
error is that it doesn't find a user in the DB, but the error
handler also fails.
Complete stack trace:
"""
[the stack trace above]
"""
Questions:
1. Why does the error handler (except block) also fail?
2. What's the correct fix — fix the handler, or prevent
it from reaching the handler?
FastAPI-Specific Runtime Errors
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"
}
]
}
What it means: The request doesn't comply with the Pydantic schema. FastAPI returns 422 automatically. What to investigate: Is the client sending the correct fields? Did the Pydantic schema change?
2. HTTPException vs unhandled exceptions
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)
# This returns 404 with a clean message:
if not task:
raise HTTPException(status_code=404, detail="Task not found")
# This returns 500 with a stack trace:
return {"title": task.name} # AttributeError if task doesn't have .name
The difference: HTTPException is a controlled error that you handle. An AttributeError is an uncontrolled error that FastAPI converts into a 500.
3. Dependency errors (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
What it means: The dependency (in this case get_db) fails before your function runs.
What to investigate: Is the database running? Are the credentials correct?
4. async/await errors
from fastapi import FastAPI
import httpx
app = FastAPI()
@app.get("/api/external-data")
async def get_external():
# Error: httpx.get is synchronous inside an async function
response = httpx.get("https://api.external.com/data")
return response.json()
This works but blocks the event loop. The correct code uses the async client:
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()
Verifying Claude Code's Diagnosis: The Checklist
After Claude Code gives you a diagnosis based on a stack trace, verify with this checklist:
□ Is the diagnosis consistent with the type of error?
(If it says "KeyError" but the trace says "TypeError", something is wrong)
□ Does the line of code Claude Code mentions match
the one in the stack trace?
□ Does the proposed fix handle the edge case that caused the error?
□ Could the fix cause a new error?
(e.g., a try/except that silences an important error)
□ Are there other places in the code with the same pattern
that could have the same bug?
Connection to the Project
How it applies to the capstone project (Module 8)
The capstone project includes runtime bugs — errors that only show up when running the application with certain inputs. You'll need to:
- Run the endpoints and capture stack traces
- Interpret the stack traces (first yourself, then with Claude Code)
- Verify that the diagnosis makes sense against the code
- Apply the fix and confirm that you don't introduce new errors
The ability to read a stack trace without AI help makes you faster. The ability to use Claude Code for complex stack traces (chained, with multiple layers of abstraction) makes you more effective.
Troubleshooting
Problem 1: "The stack trace has 50 lines and I don't know where to start"
Cause: Long stack traces include library frames (FastAPI, SQLAlchemy, Pydantic) that are internal and not your code.
Solution: Look for the frames that mention YOUR files (/app/, src/). Ignore the library frames unless the error is in how you use the library. Always start with the last line (the error) and go up to the first frame that's your code.
Problem 2: "Claude Code suggests a fix but it changes the logic"
Cause: Claude Code sometimes "fixes" the error by changing what the code does, not how it does it.
Solution: Verify that the fix keeps the expected behavior. A try/except that returns a default value "fixes" the error but can hide a bug. Ask yourself: "Should the code handle this case, or should this case not reach here?"
Problem 3: "The error only appears in production, not in development"
Cause: Environment differences: library versions, DB data, environment variables, timing.
Solution: Capture the complete stack trace from production (with logs). Verify that the library versions in requirements.txt are the same. Try to reproduce with the same data (anonymized).
Problem 4: "I don't understand what the library error means"
Cause: Errors like sqlalchemy.exc.IntegrityError or pydantic.error_wrappers.ValidationError aren't self-explanatory.
Solution: Pass the stack trace to Claude Code with the context "I don't understand what this [library] error means. Explain in simple terms what causes it." Claude Code is very good at explaining errors from popular libraries.
Exercises
Exercise 1: Read a stack trace (Easy)
Read this stack trace and answer: (a) What type of error is it? (b) In which file and line did it occur? (c) What's the probable cause?
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
See solution
(a) Type of error: KeyError — a key is looked up that doesn't exist in a dictionary.
(b) File and line: /app/services/user_service.py, line 45, in the get_user function.
(c) Probable cause: self.users is a dictionary and doesn't contain the key 42. This can mean:
- The user with ID 42 doesn't exist in the in-memory storage
- The
user_idis passed asintbut the dictionary keys arestr(or vice versa) - The
self.usersdictionary wasn't populated correctly
More robust fix:
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
Exercise 2: Diagnose a TypeError (Medium)
This stack trace appears when a user tries to create a task. Diagnose the problem and propose a 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
Context: the request's Pydantic model is:
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
See solution
Diagnosis:
The error is TypeError: can only join an iterable on the line tags=",".join(data.tags).
data.tags is Optional[list[str]], which means it can be None. When the user doesn't send tags in the request, data.tags is None, and ",".join(None) fails because None isn't 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
Note: This is an extremely common pattern in AI-generated code. Claude Code tends to generate code that works with the "happy path" (all fields present) but doesn't handle optional fields correctly.
Exercise 3: Chained stack trace (Hard)
Interpret this chained stack trace. Identify: (a) the original error, (b) the secondary error, (c) what the real bug is that you need to fix.
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'
See solution
(a) Original error: httpx.ConnectTimeout: timed out
- The external payment service didn't respond within 10 seconds
- This is a network/infrastructure error, not a code error
(b) Secondary error: AttributeError: 'ConnectTimeout' object has no attribute 'response'
- In the
exceptblock, the code tries to accesse.response.text - But
ConnectTimeouthas noresponseattribute (because there was never an HTTP response — the connection wasn't even established) - This is a bug in the error handler
(c) The real bug you need to fix:
The error handler assumes that all httpx exceptions have a .response, but ConnectTimeout occurs before receiving a response. The correct fix:
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"
)
Lessons:
- Handle different exception types separately
ConnectTimeout→ the service isn't available (503)HTTPStatusError→ the service responded with an error (this one does have.response)HTTPError→ a catch-all for other network errors
Exercise 4: Prompt for Claude Code (Medium)
Write the prompt you'd give Claude Code for this stack trace. Include the context needed for a good diagnosis.
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'
Context: the filters come from query parameters of the endpoint.
See solution
A good prompt:
My GET /api/tasks endpoint accepts date filters. When I pass
?start_date=2026-03-13, SQLAlchemy returns an error saying that
the value is neither a string nor a 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'
"""
SQLAlchemy model:
"""python
class Task(Base):
__tablename__ = "tasks"
created_at = Column(DateTime, default=datetime.utcnow)
"""
Filters schema:
"""python
class TaskFilters(BaseModel):
start_date: Optional[str] = None
end_date: Optional[str] = None
"""
Questions:
1. Why does SQLAlchemy reject '2026-03-13' if
Task.created_at is DateTime?
2. Is the problem in the filter's type (str) vs
the column's type (DateTime)?
3. What's the correct way to handle the conversion?
Why it works:
- ✅ It describes the scenario (which endpoint, which parameter)
- ✅ It includes the complete stack trace
- ✅ It includes BOTH relevant models (SQLAlchemy and Pydantic)
- ✅ It asks specific questions that guide the diagnosis
- ✅ The bug is clear:
start_dateisstrin Pydantic butcreated_atisDateTimein SQLAlchemy — it needs conversion
Expected fix: Change the type in Pydantic to Optional[datetime] or convert the string to datetime before filtering.
Exercise 5: Find the real bug (Hard)
This stack trace does NOT tell the whole truth. Read the code, compare it with the error, and determine whether the obvious fix is the correct fix.
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
Complete code of the function:
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
The context: The product has stock=10. The user requested quantity=5. It should work, but it raises "Insufficient stock". Why?
See solution
The obvious fix is incorrect. The stack trace says ValueError: Insufficient stock, which suggests that new_quantity < 0. But 10 - 5 = 5, which isn't negative.
The real bug is somewhere else. Possible causes the stack trace doesn't reveal:
-
Race condition: Another request reduced the stock between the query and the calculation. If two requests of
quantity=5arrive at the same time, both readstock=10, but when the second one commits, the stock is already 5 and should be 0, not -5. -
Incorrect data type:
quantitycould be negative (the user sent-5, and10 - (-5) = 15... wait, that would give positive). Orquantitycould be a string that was converted wrong. -
The most probable bug: The function is called with a negative
quantityto represent "return stock" (as a convention), and someone calledupdate_stock(product_id=1, quantity=-15). Then:10 - (-15) = 25, which is positive... no, that doesn't fail either.
A more careful review: If stock=10 and quantity=5 really raises the error, the problem could be that product.stock isn't 10 at the moment of the query. Causes:
- The DB has a different value than expected
- There's a trigger in the DB that modifies the stock
- Another process modified the stock between the query and the check
product.stockis None (if the column allows null)
The lesson: A stack trace tells you WHERE it fails, but not always WHY. For bugs where the data isn't what you expect, you need to add logging or use pdb to inspect the values at runtime.
Investigation fix (not the final fix):
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
Adding logging with the concrete values will tell you exactly why the condition triggers.
Summary
In this capsule you learned:
- Python stack traces are read from the bottom up: error → recent frame → chain of calls
- The 10 most common exceptions represent 80% of the errors you'll encounter
- To pass a stack trace to Claude Code, include the complete trace + relevant code + context
- Chained stack traces have an original error and an error in the handler — identify both
- FastAPI-specific errors (422, Depends, async) have recognizable patterns
- Always verify Claude Code's diagnosis: is it consistent with the error? could the fix cause a new bug?
- Sometimes the stack trace tells you where but not why — you need to add logging to investigate
Next capsule: Systematic Debugging — the complete process of reproduce → isolate → diagnose → fix → verify.
Additional resources
- Python — Built-in Exceptions - The official reference for all Python exceptions
- Real Python — Understanding Tracebacks - A detailed guide to understanding stack traces
- FastAPI — Handling Errors - Handling errors in FastAPI
- SQLAlchemy — Exceptions - Common SQLAlchemy exceptions
- Pydantic — Error Handling - Validation and errors in Pydantic v2
- httpx — Exceptions - Exceptions of the httpx HTTP client
Debugging & Code Review with Claude Code — Module 6, Capsule 03 Claude Code Agentic Development Path — Guide #6 of 11