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.py called server.py which called handlers.py which called services.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:

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

    • sum() is trying to add an int with None
    • Some task has priority_score = None
  2. 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_score as None, sum() fails
  3. 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_score is nullable and sum() doesn't handle None
  • ⚠️ 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:

  1. First error (top): NoResultFound — the query didn't find the user
  2. Second error (bottom): Inside the except that handles the NoResultFound, the code tries to use cache.get() but cache is None

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:

  1. Run the endpoints and capture stack traces
  2. Interpret the stack traces (first yourself, then with Claude Code)
  3. Verify that the diagnosis makes sense against the code
  4. 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_id is passed as int but the dictionary keys are str (or vice versa)
  • The self.users dictionary 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 except block, the code tries to access e.response.text
  • But ConnectTimeout has no response attribute (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_date is str in Pydantic but created_at is DateTime in 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:

  1. Race condition: Another request reduced the stock between the query and the calculation. If two requests of quantity=5 arrive at the same time, both read stock=10, but when the second one commits, the stock is already 5 and should be 0, not -5.

  2. Incorrect data type: quantity could be negative (the user sent -5, and 10 - (-5) = 15... wait, that would give positive). Or quantity could be a string that was converted wrong.

  3. The most probable bug: The function is called with a negative quantity to represent "return stock" (as a convention), and someone called update_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.stock is 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

  1. Python — Built-in Exceptions - The official reference for all Python exceptions
  2. Real Python — Understanding Tracebacks - A detailed guide to understanding stack traces
  3. FastAPI — Handling Errors - Handling errors in FastAPI
  4. SQLAlchemy — Exceptions - Common SQLAlchemy exceptions
  5. Pydantic — Error Handling - Validation and errors in Pydantic v2
  6. 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