Module 6: Debugging with Claude Code
Exercise: Real Debugging
Exercise: Real Debugging
Capsule overview
This is the integrative exercise for module 6. So far you've learned log analysis, stack trace interpretation, the systematic debugging process, and when Claude Code doesn't help. Now you're going to apply it all together in a FastAPI application with 5 bugs of different types.
It's not an artificial exercise. The bugs you'll find are the kind AI actually generates: a runtime error from not handling None, a logic error where the code does the opposite of what it should, an edge case that crashes with valid but unexpected inputs, a performance bug from inefficient queries, and a silent error where the data gets corrupted without raising exceptions.
For each bug, you must follow the process: reproduce → isolate → diagnose → fix → verify. And you must document your process — that's as important as the fix.
The Application: TaskFlow API
TaskFlow is a task-management API with users, categories, and statistics. It has 4 main files. Read all the code before you start debugging.
File 1: models.py
from pydantic import BaseModel, Field, field_validator
from typing import Optional
from datetime import datetime, date
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class TaskStatus(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: str
role: str = "member"
class UserResponse(BaseModel):
id: int
name: str
email: str
role: str
created_at: datetime
class CategoryCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
color: str = "#3498db"
class CategoryResponse(BaseModel):
id: int
name: str
color: str
task_count: int = 0
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = None
priority: Priority = Priority.MEDIUM
category_id: Optional[int] = None
assignee_id: Optional[int] = None
due_date: Optional[str] = None
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v):
if v is not None:
parsed = datetime.strptime(v, "%Y-%m-%d")
return v
return v
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[Priority] = None
status: Optional[TaskStatus] = None
category_id: Optional[int] = None
assignee_id: Optional[int] = None
due_date: Optional[str] = None
class TaskResponse(BaseModel):
id: int
title: str
description: Optional[str]
priority: str
status: str
category: Optional[str]
assignee: Optional[str]
due_date: Optional[str]
created_at: datetime
updated_at: Optional[datetime]
File 2: database.py
from datetime import datetime
from typing import Optional
users_db: dict[int, dict] = {}
categories_db: dict[int, dict] = {}
tasks_db: dict[int, dict] = {}
next_user_id = 1
next_category_id = 1
next_task_id = 1
def seed_data():
"""Populate the database with example data."""
global next_user_id, next_category_id, next_task_id
# Users
users = [
{"name": "Ana García", "email": "ana@taskflow.com", "role": "admin"},
{"name": "Carlos López", "email": "carlos@taskflow.com", "role": "member"},
{"name": "María Torres", "email": "maria@taskflow.com", "role": "member"},
]
for u in users:
users_db[next_user_id] = {
**u, "id": next_user_id, "created_at": datetime.utcnow()
}
next_user_id += 1
# Categories
categories = [
{"name": "Backend", "color": "#e74c3c"},
{"name": "Frontend", "color": "#3498db"},
{"name": "DevOps", "color": "#2ecc71"},
]
for c in categories:
categories_db[next_category_id] = {
**c, "id": next_category_id
}
next_category_id += 1
# Tasks
tasks = [
{
"title": "Implement JWT authentication",
"description": "Add login/register with JWT tokens",
"priority": "high",
"status": "in_progress",
"category_id": 1,
"assignee_id": 1,
"due_date": "2026-03-20",
},
{
"title": "Design dashboard",
"description": "Create mockups of the main dashboard",
"priority": "medium",
"status": "pending",
"category_id": 2,
"assignee_id": 2,
"due_date": "2026-03-25",
},
{
"title": "Set up CI/CD",
"description": "GitHub Actions pipeline",
"priority": "high",
"status": "completed",
"category_id": 3,
"assignee_id": 1,
"due_date": "2026-03-10",
},
{
"title": "Optimize queries",
"description": None,
"priority": "low",
"status": "pending",
"category_id": 1,
"assignee_id": None,
"due_date": None,
},
{
"title": "Review pull requests",
"description": "The team's pending PRs",
"priority": "medium",
"status": "pending",
"category_id": None,
"assignee_id": 3,
"due_date": "2026-03-15",
},
]
for t in tasks:
tasks_db[next_task_id] = {
**t,
"id": next_task_id,
"created_at": datetime.utcnow(),
"updated_at": None,
}
next_task_id += 1
File 3: services.py
from datetime import datetime
from typing import Optional
from database import (
users_db, categories_db, tasks_db,
next_user_id, next_category_id, next_task_id,
)
class UserService:
def get_all(self) -> list[dict]:
return list(users_db.values())
def get_by_id(self, user_id: int) -> Optional[dict]:
return users_db.get(user_id)
def create(self, data: dict) -> dict:
global next_user_id
user = {
"id": next_user_id,
"name": data["name"],
"email": data["email"],
"role": data.get("role", "member"),
"created_at": datetime.utcnow(),
}
users_db[next_user_id] = user
next_user_id += 1
return user
class CategoryService:
def get_all(self) -> list[dict]:
return list(categories_db.values())
def get_by_id(self, cat_id: int) -> Optional[dict]:
return categories_db.get(cat_id)
def get_with_task_count(self) -> list[dict]:
result = []
for cat in categories_db.values():
count = 0
for task in tasks_db.values():
if task["category_id"] == cat["id"]:
count += 1
result.append({**cat, "task_count": count})
return result
class TaskService:
def get_all(
self,
status: Optional[str] = None,
priority: Optional[str] = None,
assignee_id: Optional[int] = None,
) -> list[dict]:
tasks = list(tasks_db.values())
if status:
tasks = [t for t in tasks if t["status"] == status]
if priority:
tasks = [t for t in tasks if t["priority"] == priority]
if assignee_id:
tasks = [t for t in tasks if t["assignee_id"] == assignee_id]
return tasks
def get_by_id(self, task_id: int) -> Optional[dict]:
return tasks_db.get(task_id)
def create(self, data: dict) -> dict:
global next_task_id
task = {
"id": next_task_id,
"title": data["title"],
"description": data.get("description"),
"priority": data.get("priority", "medium"),
"status": "pending",
"category_id": data.get("category_id"),
"assignee_id": data.get("assignee_id"),
"due_date": data.get("due_date"),
"created_at": datetime.utcnow(),
"updated_at": None,
}
tasks_db[next_task_id] = task
next_task_id += 1
return task
def update(self, task_id: int, data: dict) -> Optional[dict]:
task = tasks_db.get(task_id)
if not task:
return None
for key, value in data.items():
if value is not None:
task[key] = value
task["updated_at"] = datetime.utcnow()
return task
def delete(self, task_id: int) -> bool:
if task_id in tasks_db:
del tasks_db[task_id]
return True
return False
def get_stats(self) -> dict:
all_tasks = list(tasks_db.values())
total = len(all_tasks)
by_status = {}
for task in all_tasks:
status = task["status"]
by_status[status] = by_status.get(status, 0) + 1
by_priority = {}
for task in all_tasks:
priority = task["priority"]
by_priority[priority] = by_priority.get(priority, 0) + 1
# Calculate completion rate
completed = by_status.get("completed", 0)
active = by_status.get("active", 0)
completion_rate = completed / active * 100
# Overdue tasks
overdue = []
today = datetime.utcnow().strftime("%Y-%m-%d")
for task in all_tasks:
if task["due_date"] and task["due_date"] < today:
if task["status"] not in ["completed", "cancelled"]:
overdue.append(task["title"])
return {
"total": total,
"by_status": by_status,
"by_priority": by_priority,
"completion_rate": completion_rate,
"overdue_tasks": overdue,
"overdue_count": len(overdue),
}
def get_user_workload(self) -> list[dict]:
workload = {}
for task in tasks_db.values():
uid = task["assignee_id"]
if uid not in workload:
user = users_db.get(uid)
workload[uid] = {
"user_id": uid,
"user_name": user["name"],
"total_tasks": 0,
"pending": 0,
"in_progress": 0,
"completed": 0,
}
workload[uid]["total_tasks"] += 1
status = task["status"]
if status in workload[uid]:
workload[uid][status] += 1
return sorted(
workload.values(),
key=lambda x: x["total_tasks"],
reverse=True
)
def search(self, query: str) -> list[dict]:
results = []
query_lower = query.lower()
for task in tasks_db.values():
if query_lower in task["title"].lower():
results.append(task)
elif query_lower in task["description"].lower():
results.append(task)
return results
File 4: main.py
from fastapi import FastAPI, HTTPException, Query
from typing import Optional
from models import (
UserCreate, UserResponse,
CategoryCreate, CategoryResponse,
TaskCreate, TaskUpdate, TaskResponse,
)
from services import UserService, CategoryService, TaskService
from database import seed_data, users_db, categories_db
app = FastAPI(title="TaskFlow API", version="1.0.0")
user_service = UserService()
category_service = CategoryService()
task_service = TaskService()
@app.on_event("startup")
async def startup():
seed_data()
# --- Users ---
@app.get("/api/users")
async def list_users():
return user_service.get_all()
@app.post("/api/users", status_code=201)
async def create_user(user: UserCreate):
new_user = user_service.create(user.model_dump())
return new_user
# --- Categories ---
@app.get("/api/categories")
async def list_categories():
return category_service.get_with_task_count()
# --- Tasks ---
@app.get("/api/tasks")
async def list_tasks(
status: Optional[str] = None,
priority: Optional[str] = None,
assignee_id: Optional[int] = None,
):
tasks = task_service.get_all(status, priority, assignee_id)
result = []
for task in tasks:
category_name = None
if task["category_id"]:
cat = categories_db.get(task["category_id"])
category_name = cat["name"]
assignee_name = None
if task["assignee_id"]:
user = users_db.get(task["assignee_id"])
assignee_name = user["name"]
result.append({
**task,
"category": category_name,
"assignee": assignee_name,
})
return result
@app.post("/api/tasks", status_code=201)
async def create_task(task: TaskCreate):
if task.category_id:
cat = category_service.get_by_id(task.category_id)
if not cat:
raise HTTPException(status_code=404, detail="Category not found")
if task.assignee_id:
user = user_service.get_by_id(task.assignee_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
new_task = task_service.create(task.model_dump())
return new_task
@app.get("/api/tasks/stats")
async def get_stats():
return task_service.get_stats()
@app.get("/api/tasks/search")
async def search_tasks(q: str = Query(..., min_length=1)):
return task_service.search(q)
@app.get("/api/tasks/workload")
async def get_workload():
return task_service.get_user_workload()
@app.get("/api/tasks/{task_id}")
async def get_task(task_id: int):
task = task_service.get_by_id(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.patch("/api/tasks/{task_id}")
async def update_task(task_id: int, data: TaskUpdate):
updated = task_service.update(task_id, data.model_dump(exclude_unset=True))
if not updated:
raise HTTPException(status_code=404, detail="Task not found")
return updated
@app.delete("/api/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int):
deleted = task_service.delete(task_id)
if not deleted:
raise HTTPException(status_code=404, detail="Task not found")
The 5 Bugs
Each bug is described with a user scenario. Your job is to follow the reproduce → isolate → diagnose → fix → verify process for each one.
Bug 1: The Stats Crash
Report: "When I call GET /api/tasks/stats, the server returns a 500 error."
Difficulty level: Easy
Instructions:
- Identify what type of error it is
- Use the stack trace to diagnose
- Apply a fix that doesn't change the business logic
See solution
REPRODUCE:
curl http://localhost:8000/api/tasks/stats
# Response: 500 Internal Server Error
The stack trace shows:
File "/app/services.py", line XX, in get_stats
completion_rate = completed / active * 100
ZeroDivisionError: division by zero
ISOLATE:
The error always occurs — it doesn't depend on external inputs. It's a bug in the internal logic.
DIAGNOSE:
completed = by_status.get("completed", 0)
active = by_status.get("active", 0) # BUG: "active" isn't a valid status
completion_rate = completed / active * 100
The valid statuses are: pending, in_progress, completed, cancelled. There's no "active". So active is always 0, causing division by zero.
The correct calculation of "active tasks" should be: total - cancelled or pending + in_progress + completed.
FIX:
completed = by_status.get("completed", 0)
cancelled = by_status.get("cancelled", 0)
total_active = total - cancelled
if total_active == 0:
completion_rate = 0.0
else:
completion_rate = round(completed / total_active * 100, 1)
VERIFY:
curl http://localhost:8000/api/tasks/stats
# ✅ Response: 200 with a correct completion_rate
# With the seed data: 1 completed of 4 active = 25.0%
Bug 2: The Search That Explodes
Report: "The search endpoint GET /api/tasks/search?q=optimize works with some tasks but crashes with others. The error seems random."
Difficulty level: Medium
Instructions:
- Reproduce the error with different searches
- Isolate which tasks cause the crash
- Diagnose why some tasks crash and others don't
See solution
REPRODUCE:
curl "http://localhost:8000/api/tasks/search?q=JWT"
# ✅ Works — returns the JWT task
curl "http://localhost:8000/api/tasks/search?q=optimize"
# ❌ 500 Internal Server Error
curl "http://localhost:8000/api/tasks/search?q=dashboard"
# ✅ Works
ISOLATE:
What's special about the "optimize" search? Looking at the seed data, the "Optimize queries" task has description: None.
curl "http://localhost:8000/api/tasks/search?q=review"
# Does it work? Yes — "Review pull requests" has description="The team's pending PRs"
curl "http://localhost:8000/api/tasks/search?q=pipeline"
# Does it work? Yes — "Set up CI/CD" has description="GitHub Actions pipeline"
The bug only appears when the search does NOT match the title but the code tries to search in description — and that task has description=None.
DIAGNOSE:
def search(self, query: str) -> list[dict]:
results = []
query_lower = query.lower()
for task in tasks_db.values():
if query_lower in task["title"].lower():
results.append(task)
elif query_lower in task["description"].lower(): # BUG: description can be None
results.append(task)
return results
When task["description"] is None, None.lower() raises AttributeError.
The "optimize" search matches "Optimize queries" by title, so it does NOT reach the elif. But if you search for something that isn't in that task's title, the code tries to search in description and crashes.
Wait — but does "optimize" match the title? "Optimize" in lowercase is "optimize" and the title is "Optimize queries", with "optimize" included. So it does match. Why does it crash?
Looking more carefully: the "optimize" search matches task 4 by title, but then it KEEPS iterating and reaches a task where the search fails on the title and the description is None.
Actually the bug is simpler: when ANY task has description=None, the search crashes when it reaches that task if the query doesn't match its title.
FIX:
def search(self, query: str) -> list[dict]:
results = []
query_lower = query.lower()
for task in tasks_db.values():
title_match = query_lower in task["title"].lower()
desc = task.get("description") or ""
desc_match = query_lower in desc.lower()
if title_match or desc_match:
results.append(task)
return results
VERIFY:
curl "http://localhost:8000/api/tasks/search?q=optimize"
# ✅ Returns the "Optimize queries" task
curl "http://localhost:8000/api/tasks/search?q=pipeline"
# ✅ Returns the "Set up CI/CD" task (searches in description)
curl "http://localhost:8000/api/tasks/search?q=xyz"
# ✅ Returns an empty list (doesn't crash)
Bug 3: Workload with Phantom Users
Report: "The GET /api/tasks/workload endpoint sometimes returns a 500 error. When it works, it shows incorrect data."
Difficulty level: Medium
Instructions:
- Reproduce the error
- Identify why it fails and why the data is incorrect
- Note: this bug has TWO problems — the crash AND the incorrect data
See solution
REPRODUCE:
curl http://localhost:8000/api/tasks/workload
# ❌ 500 Internal Server Error
The stack trace shows:
File "/app/services.py", line XX, in get_user_workload
workload[uid] = {
...
"user_name": user["name"],
}
AttributeError: 'NoneType' object has no attribute '__getitem__'
ISOLATE:
Looking at the seed data, task 4 ("Optimize queries") has assignee_id: None. When the code processes that task:
uid = task["assignee_id"] # uid = None
user = users_db.get(uid) # user = None (there's no user with id None)
workload[uid] = {
"user_name": user["name"], # ❌ None["name"] crashes
}
DIAGNOSE:
Problem 1 (crash): The code doesn't handle tasks without an assignee (assignee_id=None). It tries to look up a user with id=None and crashes.
Problem 2 (incorrect data): Even if you fix the crash, the status count has a bug:
if status in workload[uid]:
workload[uid][status] += 1
This only increments if the status is already a key in the dictionary. The dictionary is initialized with "pending", "in_progress", and "completed", but not "cancelled". If a task has status "cancelled", it simply isn't counted — the total doesn't add up.
FIX:
def get_user_workload(self) -> list[dict]:
workload = {}
for task in tasks_db.values():
uid = task["assignee_id"]
if uid is None:
continue
if uid not in workload:
user = users_db.get(uid)
if not user:
continue
workload[uid] = {
"user_id": uid,
"user_name": user["name"],
"total_tasks": 0,
"pending": 0,
"in_progress": 0,
"completed": 0,
"cancelled": 0,
}
workload[uid]["total_tasks"] += 1
status = task["status"]
if status in workload[uid]:
workload[uid][status] += 1
return sorted(
workload.values(),
key=lambda x: x["total_tasks"],
reverse=True
)
VERIFY:
curl http://localhost:8000/api/tasks/workload
# ✅ 200 OK, returns the workload with no error
# ✅ Doesn't include "None" as a user
# ✅ The counts add up correctly
Bug 4: The Update That Makes Data Disappear
Report: "When I update a task with PATCH /api/tasks/{id}, some fields I didn't send in the request get erased. For example, if I only change the status, the category_id disappears."
Difficulty level: Hard
Instructions:
- Reproduce with a PATCH that only sends one field
- Verify which other fields change
- This is a subtle logic bug — the code "works" but the data gets corrupted silently
See solution
REPRODUCE:
# See the current task
curl http://localhost:8000/api/tasks/1
# Result: {"id": 1, "title": "Implement JWT authentication",
# "priority": "high", "status": "in_progress", "category_id": 1,
# "assignee_id": 1, "due_date": "2026-03-20", ...}
# Update only the status
curl -X PATCH http://localhost:8000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'
# See the task after the update
curl http://localhost:8000/api/tasks/1
# Is category_id still 1? Is assignee_id still 1?
ISOLATE:
Testing different fields:
# PATCH with only title
curl -X PATCH http://localhost:8000/api/tasks/2 \
-H "Content-Type: application/json" \
-d '{"title": "New title"}'
# Verify: are the other fields intact?
DIAGNOSE:
The endpoint uses data.model_dump(exclude_unset=True):
@app.patch("/api/tasks/{task_id}")
async def update_task(task_id: int, data: TaskUpdate):
updated = task_service.update(task_id, data.model_dump(exclude_unset=True))
exclude_unset=True correctly excludes fields that weren't sent. So the data dictionary only contains {"status": "completed"}.
The service:
def update(self, task_id: int, data: dict) -> Optional[dict]:
task = tasks_db.get(task_id)
if not task:
return None
for key, value in data.items():
if value is not None: # BUG: what if I want
task[key] = value # to set a field to None?
task["updated_at"] = datetime.utcnow()
return task
The subtle bug: The condition if value is not None prevents setting a field to None intentionally. If you send {"assignee_id": null} to unassign a task, the null becomes None in Python, and the condition filters it out — the field is NOT updated.
# Try to unassign a task
curl -X PATCH http://localhost:8000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"assignee_id": null}'
# The assignee_id is STILL 1 — the null was ignored
But wait — the report says the fields "disappear", which is the opposite of what we found (fields that do NOT change when they should). Let's review again...
Looking more carefully at the flow: model_dump(exclude_unset=True) returns only the fields sent. If the PATCH body is {"status": "completed"}, the dict is only {"status": "completed"}. The for loop only modifies status. The other fields should stay intact.
The real bug from the report might be in the response: the list tasks endpoint (GET /api/tasks) enriches the data with category and assignee names, but the individual GET endpoint (GET /api/tasks/{id}) returns the raw data from the dict. Depending on which endpoint the frontend uses to verify, it can look different.
However, there's another bug here: exclude_unset=True + if value is not None creates an inconsistency. If a field has a default of None in TaskUpdate and is NOT sent, exclude_unset=True excludes it (correct). But if it's sent explicitly as null, it includes it in the dict as None, and the if value is not None ignores it.
FIX:
def update(self, task_id: int, data: dict) -> Optional[dict]:
task = tasks_db.get(task_id)
if not task:
return None
for key, value in data.items():
task[key] = value
task["updated_at"] = datetime.utcnow()
return task
We remove the if value is not None condition because exclude_unset=True already takes care of not including unsent fields. If someone explicitly sends null, it's because they want the field to be None.
VERIFY:
# Unassign a task
curl -X PATCH http://localhost:8000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"assignee_id": null}'
curl http://localhost:8000/api/tasks/1
# ✅ assignee_id is null
# Update only the status without affecting other fields
curl -X PATCH http://localhost:8000/api/tasks/2 \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'
curl http://localhost:8000/api/tasks/2
# ✅ status is "completed", other fields intact
Bug 5: The Date Validation That Doesn't Validate
Report: "I can create tasks with impossible dates like '2026-02-30' without the API giving an error. The task is created but then the endpoints that filter by date behave unpredictably."
Difficulty level: Hard
Instructions:
- Reproduce: create a task with an impossible date
- Investigate why the validator doesn't catch the invalid date
- Diagnose what problems the invalid date causes in other endpoints
See solution
REPRODUCE:
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2026-02-30"}'
# Does it return 201 or 422?
ISOLATE:
Looking at the validator in models.py:
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v):
if v is not None:
parsed = datetime.strptime(v, "%Y-%m-%d") # RAISES ValueError
return v # BUT the error rises as a Pydantic ValidationError
return v
Hmm — datetime.strptime("2026-02-30", "%Y-%m-%d") should raise ValueError: day is out of range for month. And Pydantic should turn it into a ValidationError and FastAPI should return 422.
Let's test again:
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2026-02-30"}'
# If it returns 422 → the validator works for this case
# If it returns 201 → the validator has a bug
If it returns 422, then the validator DOES work for impossible dates, but the question is: does it validate the format but not the semantics? Let's test:
# Date in the correct format but in the past
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2020-01-01"}'
# Is it created? → Yes, it doesn't validate that the date is in the future
# Date in an incorrect format
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "30-02-2026"}'
# Does it return 422? → It should
DIAGNOSE:
The validator's real bugs are:
-
It doesn't validate that the date is in the future. You can create a task with a
due_datein the past. -
It parses but doesn't use the result. The validator does
parsed = datetime.strptime(v, "%Y-%m-%d")and thenreturn v— it returns the original string, not the datetime. If you wanted to compare with the current date, you'd have to parse again. -
Date comparisons as strings are problematic. In
get_stats():
if task["due_date"] and task["due_date"] < today:
This compares strings, not dates. The lexicographic comparison of strings in ISO format ("2026-03-15" < "2026-03-20") works correctly for the same format, but it's fragile.
FIX:
@field_validator('due_date')
@classmethod
def validate_due_date(cls, v):
if v is None:
return v
try:
parsed = datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise ValueError(
f"Invalid date: '{v}'. Use YYYY-MM-DD format with valid values."
)
if parsed.date() < date.today():
raise ValueError(
f"Due date cannot be in the past: {v}"
)
return v
VERIFY:
# Impossible date
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2026-02-30"}'
# ✅ 422: Invalid date
# Date in the past
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2020-01-01"}'
# ✅ 422: Due date cannot be in the past
# Valid date
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test", "due_date": "2026-12-31"}'
# ✅ 201: Task created
# No date (optional)
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test"}'
# ✅ 201: Task created with no date
Documentation Format
For each bug you debug, document your process with this format:
## Bug [number]: [descriptive name]
### Reproduce
- Endpoint: [method + URL]
- Input: [body/params]
- Expected result: [what should happen]
- Result obtained: [what actually happens]
- Consistent: [yes/no, N of M attempts]
### Isolate
- Minimum input that causes the error: [...]
- What input DOES work: [...]
- Conclusion: the bug is related to [...]
### Diagnose
- Tool used: [Claude Code / pdb / logging / manual inspection]
- File and line: [...]
- Root cause: [...]
- Did Claude Code help? [yes/no/partially — why]
### Fix
- Changes made: [description]
- Type of fix: [root / patch]
- Code before: [...]
- Code after: [...]
### Verify
- [ ] The original input works correctly
- [ ] Inputs that worked before still work
- [ ] Edge cases tested: [list]
- [ ] Doesn't break other endpoints: [verified]
Connection to the Project
How it connects to the capstone project (Module 8)
This exercise is a simplified version of the capstone project. In module 8, the application will be larger (8-12 files), the bugs will be more varied (including hallucinations and security holes), and you'll need to combine code review with debugging. The process you practiced here — and the documentation you generated — is exactly what you'll do at a larger scale.
Troubleshooting
Problem 1: "I can't get the application to run"
Cause: Missing dependencies or an import error.
Solution: Install FastAPI and uvicorn: pip install fastapi uvicorn. Run with uvicorn main:app --reload.
Problem 2: "I found a bug but I don't know if it's one of the 5"
Cause: You might have found an additional bug that isn't on the list. Solution: Document it anyway. If you find extra bugs, that shows a good eye. The list of 5 bugs is the minimum — if you find more, it's a plus.
Problem 3: "My fix for one bug breaks another"
Cause: The bugs can interact with each other. Solution: Fix the bugs in order of independence: start with the ones that don't depend on others (Bug 1 and Bug 2), then the ones that could interact (Bug 3-5).
Problem 4: "I'm not sure if my fix is correct"
Cause: Some bugs have multiple possible fixes. Solution: The correct fix is the one that: (1) resolves the reported problem, (2) doesn't introduce new bugs, (3) handles reasonable edge cases, and (4) is a root-cause fix, not a patch. If your fix meets these 4 criteria, it's correct — even if it's different from the suggested solution.
Summary
In this exercise you applied:
- The complete systematic process: reproduce → isolate → diagnose → fix → verify on 5 different bugs
- Log analysis and stack traces: interpreting runtime errors to find the root cause
- Different types of bugs: runtime crash (ZeroDivisionError), null handling (AttributeError), incorrect logic (corrupted data), incomplete validation
- Claude Code as a tool: to diagnose known errors, not as an oracle
- Process documentation: not just the fix but how you reached the fix
This module closes Phase 2. Now you have a complete toolkit: professional code review (modules 4-5) and debugging with AI assistance (module 6). In Phase 3, you'll add advanced tools (subagents, regenerate vs edit) and apply it all in the capstone project.
Additional resources
- FastAPI — Complete Tutorial - A reference for understanding the endpoints and validations
- Pydantic v2 — Validators - Documentation of validators like the ones used in TaskCreate
- Python — datetime Module - A reference for dates and timestamps
- Real Python — Python Debugging - Debugging techniques for when Claude Code isn't enough
- Anthropic — Claude Code Best Practices - How to use Claude Code effectively for debugging
Debugging & Code Review with Claude Code — Module 6, Capsule 06 Claude Code Agentic Development Path — Guide #6 of 11