Module 4: Code Review of AI Output
Professional Code Review Checklist for AI Code
Professional Code Review Checklist for AI Code
Capsule overview
In the previous capsule you learned to prioritize: the pyramid tells you what to review first. Now you need to know what exactly to review at each level. This capsule builds your professional code review checklist for AI-generated code — the most important artifact in the entire module.
This isn't a generic "best practices" checklist. Each item is specific, verifiable, and actionable. It doesn't say "review the code" — it says "verify that each import exists by running pip show <package> or looking it up in the official documentation." It doesn't say "review security" — it says "confirm there's no string concatenation in SQL queries."
The checklist has 20 items organized into 5 categories, aligned with the priority pyramid. It's a living tool: you use it today, adapt it tomorrow, improve it with each review. By the end of your career, this checklist will be different — but it will always have as its base what you build here.
Checklist Principles
Before you see the items, understand the principles that guide them:
1. Specific and verifiable
❌ Bad: "Review the code's security"
✅ Good: "Verify there are no API keys, passwords, or tokens
hardcoded in the source code"
❌ Bad: "Verify that the code works"
✅ Good: "Run the endpoint with a valid input and an invalid one
to verify the happy path and error handling"
2. Actionable in under 5 minutes
Each checklist item should be verifiable in 1-5 minutes. If an item takes longer, it needs to be split into sub-items.
3. Prioritized by impact
The first items on the checklist are the most important. If you only review the first 5, you cover the highest risks.
4. Adapted to AI code
Each item exists because AI makes this error frequently. Items that aren't relevant for AI (like "verify there are no merge conflicts") aren't included.
The Checklist: 20 Items in 5 Categories
Category 1: Security (Items 1-5)
These are reviewed always, no exception. No matter the size of the PR, the level of urgency, or your confidence in the code.
Item 1: There are no hardcoded secrets in the code
WHAT TO VERIFY:
- API keys (Stripe, SendGrid, AWS, etc.)
- Passwords and access tokens
- Database connection strings
- Encryption keys
HOW TO VERIFY:
- Search for suspicious strings: "sk_", "Bearer ", "password",
"secret", "key", "token"
- Verify that os.getenv() does NOT have a fallback with real values
- Review configuration files (.env.example, config.py)
EXAMPLE OF A FAILURE (AI generates frequently):
# AI generates this frequently
SECRET_KEY = os.getenv("SECRET_KEY", "my-development-secret-key")
DATABASE_URL = "postgresql://admin:password123@localhost:5432/mydb"
STRIPE_KEY = "sk_live_abc123def456"
# Correct version
SECRET_KEY = os.environ["SECRET_KEY"] # Fails if it doesn't exist — good
DATABASE_URL = os.environ["DATABASE_URL"]
STRIPE_KEY = os.environ["STRIPE_API_KEY"]
SEVERITY IF IT FAILS: Critical
ESTIMATED TIME: 1-2 minutes
Item 2: SQL queries use parameterized queries (no string concatenation)
WHAT TO VERIFY:
- Any SQL query built with f-strings, .format(), or + concatenation
- User inputs that reach queries directly
- ORM queries that allow raw SQL
HOW TO VERIFY:
- Search for: f"SELECT, f"INSERT, f"UPDATE, f"DELETE
- Search for: .format( in the context of queries
- Search for: cursor.execute( with concatenated strings
EXAMPLE OF A FAILURE:
# SQL Injection — AI generates this regularly with sqlite3
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# With .format() — just as dangerous
query = "SELECT * FROM users WHERE id = {}".format(user_id)
cursor.execute(query)
# Correct — a parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
# With SQLAlchemy
stmt = select(User).where(User.name == name)
SEVERITY IF IT FAILS: Critical
ESTIMATED TIME: 2-3 minutes
Item 3: Sensitive endpoints have authentication and authorization
WHAT TO VERIFY:
- Endpoints that modify data (POST, PUT, PATCH, DELETE)
- Endpoints that expose sensitive data
- Admin endpoints
- That auth !== only authentication — also authorization
(CAN the user perform this action?)
HOW TO VERIFY:
- Review each endpoint: does it have Depends() with auth?
- Verify that the roles/permissions are correct
- Look for endpoints that access other users' data
EXAMPLE OF A FAILURE:
# No auth — anyone can delete users
@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
return {"status": "deleted"}
# With auth and authorization
@app.delete("/users/{user_id}")
async def delete_user(
user_id: int,
current_user: User = Depends(get_current_admin_user),
):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Not authorized")
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
return {"status": "deleted"}
SEVERITY IF IT FAILS: Critical
ESTIMATED TIME: 2-3 minutes
Item 4: User inputs are validated and sanitized
WHAT TO VERIFY:
- Inputs that arrive as query params, path params, or body
- Correct types (int vs str, etc.)
- Reasonable ranges (age > 0, price >= 0, page >= 1)
- Maximum length on strings
- Expected formats (email, URL, date)
HOW TO VERIFY:
- Review Pydantic models: do they have Field() with constraints?
- Review Query() and Path(): do they have ge, le, min_length, max_length?
- Are there inputs that arrive as `dict` or `Any`? — Red flag
EXAMPLE OF A FAILURE:
# No validation — accepts anything
@app.post("/products")
async def create_product(data: dict):
save_product(data)
return data
# With complete validation
class ProductCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
price: Decimal = Field(..., gt=0, le=999999)
category: str = Field(..., min_length=1, max_length=50)
description: Optional[str] = Field(None, max_length=5000)
@app.post("/products", response_model=ProductResponse)
async def create_product(product: ProductCreate):
return save_product(product)
SEVERITY IF IT FAILS: High
ESTIMATED TIME: 2-3 minutes
Item 5: Error messages don't expose internal information
WHAT TO VERIFY:
- Stack traces in HTTP responses
- Filesystem paths
- DB table/column names
- Software versions
- Configuration details
HOW TO VERIFY:
- Search for: str(e), str(exc), repr(e) in error handlers
- Search for: detail= with variables that could expose info
- Verify that except blocks use generic messages
EXAMPLE OF A FAILURE:
# Exposes internal information
@app.get("/users/{user_id}")
async def get_user(user_id: int):
try:
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
return user
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Could expose: "relation 'users' does not exist"
# or filesystem paths
# Generic message for the client, detail in the logs
import logging
logger = logging.getLogger(__name__)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
try:
user = db.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching user {user_id}: {e}")
raise HTTPException(
status_code=500,
detail="Internal server error",
)
SEVERITY IF IT FAILS: Medium-High
ESTIMATED TIME: 1-2 minutes
Category 2: Business Logic (Items 6-9)
These are reviewed whenever the code implements business rules. If it's pure boilerplate with no logic, you can move quickly.
Item 6: The code solves the problem you asked for (not a different one)
WHAT TO VERIFY:
- Does the endpoint do what you specified in the prompt?
- Is there functionality nobody asked for?
- Is functionality you did ask for missing?
- Is the approach the one you expected?
HOW TO VERIFY:
- Compare your original prompt/requirement with the generated code
- List: what you asked for, what you got, what's extra, what's missing
THIS ITEM IS SPECIFIC TO AI:
A human developer rarely solves a different problem than the one asked.
AI does it frequently — it solves a SIMILAR but not identical problem.
The code looks correct because it solves A problem,
just not yours.
SEVERITY IF IT FAILS: High
ESTIMATED TIME: 2-3 minutes
Item 7: Calculations and conditions are correct
WHAT TO VERIFY:
- Comparison operators: > vs >=, < vs <=, == vs !=
- Mathematical calculations: order of operations, rounding, precision
- Data type for money: Decimal, not float
- Business formulas: discounts, taxes, commissions, fees
HOW TO VERIFY:
- Trace manually with 2-3 example values
- Pay special attention to boundary values (the exact limit)
- Verify against the documented business rule
EXAMPLE OF A FAILURE:
# "A 10% discount for purchases over $100"
# AI generates: (is it > or >= ?)
def apply_discount(total: Decimal) -> Decimal:
if total >= Decimal("100"): # >= when it should be >
return total * Decimal("0.90")
return total
# Manual verification:
# total = 100 → with >=, applies discount (correct? "over" = >)
# total = 100.01 → with >, applies discount (correct)
# total = 99.99 → doesn't apply (correct in both)
SEVERITY IF IT FAILS: High
ESTIMATED TIME: 3-5 minutes (requires thinking with real values)
Item 8: States and transitions are valid
WHAT TO VERIFY:
- Are the possible states the correct ones?
- Are the state transitions valid?
(e.g., you can't go from "cancelled" to "active")
- Are there missing states?
- Are there transitions that shouldn't exist?
HOW TO VERIFY:
- Draw the state diagram mentally (or on paper)
- Verify that the code doesn't allow invalid transitions
- Look for: where the status is changed and what validations exist
EXAMPLE OF A FAILURE:
# AI allows any transition — there's no validation
@app.patch("/orders/{order_id}/status")
async def update_order_status(order_id: str, new_status: str):
order = get_order(order_id)
order.status = new_status # From "delivered" to "pending"? Yes, it allows it
save_order(order)
return order
# Validated transitions
VALID_TRANSITIONS = {
"pending": ["confirmed", "cancelled"],
"confirmed": ["processing", "cancelled"],
"processing": ["shipped", "cancelled"],
"shipped": ["delivered"],
"delivered": ["returned"],
"cancelled": [],
"returned": [],
}
@app.patch("/orders/{order_id}/status")
async def update_order_status(order_id: str, new_status: OrderStatus):
order = get_order(order_id)
valid_next = VALID_TRANSITIONS.get(order.status, [])
if new_status.value not in valid_next:
raise HTTPException(
status_code=400,
detail=f"Cannot transition from {order.status} to {new_status.value}",
)
order.status = new_status.value
save_order(order)
return order
SEVERITY IF IT FAILS: High
ESTIMATED TIME: 3-5 minutes
Item 9: Operations that must be atomic are
WHAT TO VERIFY:
- Sequences of operations that must all complete or none
- Money transfers (debit + credit)
- Inventory operations (reserve + charge)
- Multi-step workflows (create user + send email + assign role)
HOW TO VERIFY:
- For each sequence: what happens if step N fails?
- Is there rollback? Are there DB transactions?
- Are there idempotency keys to prevent duplicates?
EXAMPLE OF A FAILURE:
# Not atomic — if save_transfer fails, the sender already lost money
async def transfer(sender_id: str, receiver_id: str, amount: Decimal):
sender = get_account(sender_id)
sender.balance -= amount
save_account(sender) # If this happens...
receiver = get_account(receiver_id)
receiver.balance += amount
save_account(receiver) # ...but this fails → money disappeared
save_transfer(sender_id, receiver_id, amount)
# Atomic with a DB transaction
async def transfer(sender_id: str, receiver_id: str, amount: Decimal):
async with db.transaction():
sender = await get_account_for_update(sender_id)
if sender.balance < amount:
raise InsufficientFundsError()
sender.balance -= amount
receiver = await get_account_for_update(receiver_id)
receiver.balance += amount
await save_transfer(sender_id, receiver_id, amount)
SEVERITY IF IT FAILS: High-Critical (depending on the context)
ESTIMATED TIME: 2-3 minutes
Category 3: Edge Cases (Items 10-13)
These are reviewed frequently, especially in code that receives input from users or external systems.
Item 10: Handles null/None, empty, and default values
WHAT TO VERIFY:
- What happens if an Optional field is None?
- What happens with empty lists?
- What happens with empty strings ("" vs None)?
- Are the defaults reasonable?
HOW TO VERIFY:
- For each Optional input: follow the flow with a None value
- For each list: does it work with []?
- Search for: accesses to .attribute without a None check
EXAMPLE OF A FAILURE:
# Crashes if user.address is None
def get_shipping_zone(user: User) -> str:
return user.address.state # AttributeError if address is None
# Handles None correctly
def get_shipping_zone(user: User) -> str:
if not user.address:
raise ValueError("User has no shipping address configured")
return user.address.state
SEVERITY IF IT FAILS: Medium
ESTIMATED TIME: 2-3 minutes
Item 11: Error handling is complete and correct
WHAT TO VERIFY:
- Are the expected errors handled? (DB not found, service timeout)
- Are the HTTP status codes correct? (404 vs 400 vs 500)
- Do the errors propagate correctly?
- Are the try/except not too broad? (no `except Exception`)
HOW TO VERIFY:
- For each operation that can fail: is there handling?
- Are the except blocks specific?
- Is a client error (4xx) distinguished from a server error (5xx)?
EXAMPLE OF A FAILURE:
# Catch too broad — hides real bugs
@app.get("/users/{user_id}")
async def get_user(user_id: int):
try:
return db.get_user(user_id)
except Exception:
raise HTTPException(status_code=404, detail="Not found")
# And if the error is a connection timeout?
# It returns 404 instead of 500/503
# Specific handling by type of error
@app.get("/users/{user_id}")
async def get_user(user_id: int):
try:
user = db.get_user(user_id)
except ConnectionError:
raise HTTPException(status_code=503, detail="Service unavailable")
except DatabaseError as e:
logger.error(f"Database error: {e}")
raise HTTPException(status_code=500, detail="Internal error")
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
SEVERITY IF IT FAILS: Medium-High
ESTIMATED TIME: 2-3 minutes
Item 12: Pagination and limits are correct
WHAT TO VERIFY:
- Is the page calculation correct? (ceil, not floor)
- What happens with page 0 or negative?
- Is there a maximum limit on per_page/limit?
- Is the pagination done in the DB or does it load everything into memory?
HOW TO VERIFY:
- Calculate manually: 21 items / 20 per_page = 2 or 1 pages?
- Test: page=0, page=-1, per_page=0, per_page=999999
SEVERITY IF IT FAILS: Medium
ESTIMATED TIME: 1-2 minutes
Item 13: Concurrent operations don't cause race conditions
WHAT TO VERIFY:
- Can two simultaneous requests corrupt data?
- Is there read-modify-write without locking?
- Are counters incremented atomically?
- Do inventory reservations use locking?
HOW TO VERIFY:
- For each write operation: what happens if 2 requests arrive at the
same time?
- Search for: read → modify → write patterns without a transaction
EXAMPLE OF A FAILURE:
# Race condition in inventory
@app.post("/purchase/{product_id}")
async def purchase(product_id: str):
product = get_product(product_id)
if product.stock > 0: # Thread A reads stock=1
product.stock -= 1 # Thread A: stock=0
save_product(product) # Thread A saves
# Thread B also read stock=1 BEFORE A saved
# Thread B: stock=0, sells the same → oversold
return {"status": "purchased"}
raise HTTPException(status_code=400, detail="Out of stock")
# With optimistic locking
@app.post("/purchase/{product_id}")
async def purchase(product_id: str):
async with db.transaction():
product = await get_product_for_update(product_id)
if product.stock <= 0:
raise HTTPException(status_code=400, detail="Out of stock")
product.stock -= 1
await save_product(product)
return {"status": "purchased"}
SEVERITY IF IT FAILS: High (in inventory/financial operations)
ESTIMATED TIME: 2-3 minutes
Category 4: AI-Specific (Items 14-17)
These items only exist because the code was generated by AI. You wouldn't find them in a traditional code review checklist.
Item 14: All imports exist and are correct
WHAT TO VERIFY:
- Does each imported package exist in pip/PyPI?
- Do the imported functions/classes exist in those packages?
- Do the imports match the version of the package you use?
- Are there imports of functions that sound real but don't exist?
HOW TO VERIFY:
- For each non-standard import: verify in the package's docs
- Search on PyPI whether the package exists
- If an import sounds "off" but plausible → verify
EXAMPLE OF A FAILURE (classic hallucination):
from fastapi import FastAPI, BackgroundScheduler # BackgroundScheduler doesn't exist in FastAPI
from pydantic import BaseModel, validate_email # validate_email isn't from pydantic
from sqlalchemy.ext.async import AsyncSession # The correct path is sqlalchemy.ext.asyncio
from sklearn.metrics import roc_auc_multiclass # Doesn't exist, it's roc_auc_score
SEVERITY IF IT FAILS: High (the code doesn't run)
ESTIMATED TIME: 2-3 minutes
Item 15: There's no over-engineering for the problem
WHAT TO VERIFY:
- Are there design patterns that aren't needed? (Factory, Strategy,
Observer for a simple CRUD)
- Are there abstractions that have only one implementation?
- Is the code's complexity proportional to the problem's
complexity?
- Are there abstract classes with no concrete implementations?
HOW TO VERIFY:
- Compare: how simple is the problem vs how complex
is the code?
- Count classes/files: are they proportional to the functionality?
- Search for: abc.ABC, @abstractmethod, __init_subclass__
EXAMPLE OF A FAILURE:
# For an endpoint that returns "Hello, World"
# AI generates a system with Factory, Strategy, and Registry:
class GreetingStrategy(abc.ABC):
@abc.abstractmethod
def greet(self, name: str) -> str: ...
class EnglishGreeting(GreetingStrategy):
def greet(self, name: str) -> str:
return f"Hello, {name}"
class GreetingFactory:
_strategies: dict = {}
@classmethod
def register(cls, lang: str, strategy: GreetingStrategy):
cls._strategies[lang] = strategy
@classmethod
def get(cls, lang: str) -> GreetingStrategy:
return cls._strategies[lang]
# When all you need is:
@app.get("/hello/{name}")
async def hello(name: str):
return {"message": f"Hello, {name}"}
SEVERITY IF IT FAILS: Low-Medium (it works, but it's unnecessarily complex)
ESTIMATED TIME: 1-2 minutes
Item 16: Library APIs and functions are used correctly
WHAT TO VERIFY:
- Are the functions called with the correct parameters?
- Do the parameters have the correct names?
- Is the current version of the API used, not a deprecated one?
- Are the return values handled correctly?
HOW TO VERIFY:
- Compare each library call with the official documentation
- Search for: deprecation warnings, API versions
- Verify that the named parameters are correct
EXAMPLE OF A FAILURE:
# AI uses an API from an earlier version
import jwt
# pyjwt < 2.0 (deprecated)
token = jwt.encode(payload, secret, algorithm="HS256")
# In pyjwt >= 2.0, encode() returns str, not bytes
# AI might add .decode("utf-8") which is no longer necessary
decoded = jwt.decode(token, secret, algorithm="HS256")
# In pyjwt >= 2.0, the parameter is algorithms=[...], not algorithm=...
# This raises a subtle error
# Correct version for pyjwt >= 2.0
import jwt
token = jwt.encode(payload, secret, algorithm="HS256") # Returns str
decoded = jwt.decode(token, secret, algorithms=["HS256"]) # A list, not a string
SEVERITY IF IT FAILS: Medium-High (can cause subtle runtime errors)
ESTIMATED TIME: 3-5 minutes (requires checking docs)
Item 17: There's no code that mixes patterns from different frameworks
WHAT TO VERIFY:
- Is Flask mixed with FastAPI?
- Is Django ORM mixed with SQLAlchemy?
- Are patterns from one framework used in another?
- Are the decorators and middleware from the correct framework?
HOW TO VERIFY:
- Verify that all the imports are from the same ecosystem
- Search for patterns that "look off" for the framework used
- Verify that the decorators correspond to the framework
EXAMPLE OF A FAILURE:
from fastapi import FastAPI
from flask import jsonify # Flask in a FastAPI project?
app = FastAPI()
@app.route("/users") # @app.route is Flask, not FastAPI
def get_users(): # No async — a Flask pattern, not FastAPI
users = get_all_users()
return jsonify(users) # jsonify is Flask — FastAPI uses a direct return
SEVERITY IF IT FAILS: Medium (can cause hard-to-diagnose errors)
ESTIMATED TIME: 1-2 minutes
Category 5: Code Quality (Items 18-20)
These are reviewed when there's time. They're important for maintainability but don't block the merge.
Item 18: The response models filter sensitive data
WHAT TO VERIFY:
- Do the endpoints return only the necessary fields?
- Are there password_hash, tokens, or internal data in the response?
- Are the Pydantic response_model defined?
- Is SELECT * turned into a filtered response?
HOW TO VERIFY:
- Compare: which fields does the DB model have vs what does
the endpoint return?
- Search for: response_model= in each endpoint
EXAMPLE OF A FAILURE:
# Returns everything, including password_hash
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return db.get_user(user_id)
# Response: {"id": 1, "name": "Ana", "email": "...",
# "password_hash": "$2b$12$...", "role": "admin"}
# Returns only public fields
class UserPublic(BaseModel):
id: int
name: str
email: str
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
return db.get_user(user_id)
SEVERITY IF IT FAILS: Medium-High (data leak)
ESTIMATED TIME: 1-2 minutes
Item 19: The code structure follows the project's conventions
WHAT TO VERIFY:
- Are the new files in the correct directory?
- Do the file names follow the conventions?
- Are the functions in the correct module?
(no business logic in routes)
- Do the imports follow the project's order?
HOW TO VERIFY:
- Compare the structure of the new code with existing code
- Verify: service logic in service.py or in routes.py?
SEVERITY IF IT FAILS: Low
ESTIMATED TIME: 1 minute
Item 20: The tests (if they exist) verify real behavior
WHAT TO VERIFY:
- Do the tests verify correct behavior, not just
that it "doesn't crash"?
- Are there tests for the identified edge cases?
- Do the assertions verify specific values?
- Are the tests independent of each other?
- If AI generated the tests, do they verify the right thing or just
confirm what AI thinks is correct?
HOW TO VERIFY:
- Read each assertion: what exactly does it verify?
- Search for: tests that only verify status_code 200 without
checking the body
- Search for: tests with assert True (they verify nothing)
EXAMPLE OF A FAILURE:
# Test that verifies nothing useful
def test_create_user():
response = client.post("/users", json={"name": "Ana", "email": "ana@test.com"})
assert response.status_code == 200 # Only verifies it doesn't crash
# Doesn't verify: was the user created? Is the data correct?
# Was the password hashed? Does the response have the correct format?
# Test that verifies real behavior
def test_create_user():
response = client.post(
"/users",
json={"name": "Ana", "email": "ana@test.com", "password": "secure123"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Ana"
assert data["email"] == "ana@test.com"
assert "password" not in data
assert "password_hash" not in data
assert "id" in data
SEVERITY IF IT FAILS: Medium
ESTIMATED TIME: 3-5 minutes
Pocket Checklist: Quick Version
For daily use, this is the compact version you can keep on hand:
AI CODE REVIEW — QUICK CHECKLIST
==========================================
SECURITY (always):
□ 1. No hardcoded secrets
□ 2. SQL uses parameterized queries
□ 3. Sensitive endpoints have auth
□ 4. Inputs validated and sanitized
□ 5. Errors don't expose internal info
LOGIC (whenever there are rules):
□ 6. Solves the problem asked for (not a different one)
□ 7. Calculations and conditions correct (> vs >=)
□ 8. Valid state transitions
□ 9. Atomic operations when necessary
EDGE CASES (frequently):
□ 10. Handles None/empty/defaults
□ 11. Complete and correct error handling
□ 12. Correct pagination/limits
□ 13. No race conditions in writes
AI-SPECIFIC (always in AI code):
□ 14. All imports exist
□ 15. No over-engineering
□ 16. Library APIs used correctly
□ 17. Doesn't mix frameworks
QUALITY (when there's time):
□ 18. Response models filter sensitive data
□ 19. Structure follows the project's conventions
□ 20. Tests verify real behavior
Connection to the Project
The checklist as a tool for the capstone project
In module 8, you're going to receive a FastAPI codebase with 15-20 problems. Your checklist is your main weapon. Each checklist item corresponds to a type of problem you might find:
| Checklist item | Typical problem in M8 |
|---|---|
| Item 1 (secrets) | Hardcoded API key in config.py |
| Item 2 (SQL) | String concatenation in a search query |
| Item 7 (calculations) | Discount calculated with the wrong operator |
| Item 14 (imports) | Import of a function that doesn't exist in the library |
| Item 15 (over-engineering) | Factory pattern for a single type of object |
If your checklist doesn't have an item for a type of problem, you won't find it. That's why the checklist is a living tool — you improve it every time a problem slips past you.
Troubleshooting
Problem 1: "20 items is too many — I can't remember them all"
Cause: You don't need to memorize them. You need to internalize them. Solution: Start with the first 5 (security). Use them in every review. When they're automatic, add the next 4 (logic). Progress gradually. In 2-3 weeks, all 20 will be second nature. Meanwhile, keep the pocket checklist visible.
Problem 2: "Some items don't apply to my code"
Cause: Not all code has SQL, state transitions, or concurrency. Solution: Mark "N/A" on items that don't apply and move on. The checklist covers the most common cases — if an item doesn't apply, it's 5 seconds of "N/A", not 5 minutes of review. Never remove items from the checklist; better to have an item that doesn't apply 80% of the time than to miss a critical issue 20% of the time.
Problem 3: "Item 16 (correct APIs) takes a lot of time"
Cause: Verifying each library's documentation is slow.
Solution: You don't verify EVERY call. You verify: (1) imports you don't recognize, (2) functions with parameters that sound off, (3) anything that raises doubt. For standard calls you've used 100 times (like FastAPI() or BaseModel), trust your experience. Item 16 is for detecting subtle hallucinations, not for re-verifying all the documentation.
Problem 4: "My team has its own checklist — do I use both?"
Cause: Existing checklists are probably for human code. Solution: Merge them. Your team checklist probably has Category 1 (security) and Category 5 (quality) items. The Category 4 (AI-Specific) items probably aren't there. Add items 14-17 to your team's checklist. Propose the change as "additional items for AI-generated code."
Exercises
Exercise 1: Apply the checklist (Medium)
Apply the 20-item checklist to the following code. For each item, mark: ✅ (passes), ❌ (fails), ⚠️ (partial), or N/A.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
import uuid
app = FastAPI()
products_db = {}
class Product(BaseModel):
name: str
price: float
category: str
stock: int
class ProductResponse(Product):
id: str
created_at: datetime
@app.post("/products", response_model=ProductResponse)
async def create_product(product: Product):
product_id = str(uuid.uuid4())
record = ProductResponse(
id=product_id,
created_at=datetime.utcnow(),
**product.model_dump(),
)
products_db[product_id] = record
return record
@app.get("/products", response_model=List[ProductResponse])
async def list_products(category: Optional[str] = None):
products = list(products_db.values())
if category:
products = [p for p in products if p.category == category]
return products
@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: str):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
return products_db[product_id]
See solution
| # | Item | Result | Note |
|---|---|---|---|
| 1 | Hardcoded secrets | ✅ | No secrets |
| 2 | SQL parameterized | N/A | No SQL |
| 3 | Auth on endpoints | ⚠️ | POST with no auth — should it have any? |
| 4 | Inputs validated | ⚠️ | No min_length, price can be negative |
| 5 | Errors don't expose info | ✅ | Only "Product not found" |
| 6 | Solves the problem asked | ✅ | Correct basic CRUD |
| 7 | Correct calculations | N/A | No calculations |
| 8 | State transitions | N/A | No states |
| 9 | Atomic operations | N/A | Single-step operations |
| 10 | Handles None/empty | ✅ | Optional category works with None |
| 11 | Error handling | ⚠️ | Only 404, no handling for other errors |
| 12 | Pagination | ❌ | No pagination — returns EVERYTHING |
| 13 | Race conditions | N/A | In-memory, single process |
| 14 | Imports exist | ✅ | They're all real imports |
| 15 | Over-engineering | ✅ | Simple and proportional |
| 16 | Correct APIs | ✅ | FastAPI and Pydantic used correctly |
| 17 | Mixes frameworks | ✅ | Only FastAPI |
| 18 | Response filters data | ✅ | ProductResponse has no sensitive data |
| 19 | Structure conventions | ✅ | Standard structure |
| 20 | Tests | N/A | No tests |
Summary: 2 main issues:
price: floatshould beDecimalwithgt=0— accepts negatives and has imprecision- No pagination in list_products — dangerous with many products
Exercise 2: Improve a checklist item (Medium)
Item 4 says: "User inputs are validated and sanitized."
Rewrite this item to make it more specific and verifiable. Your version should include:
- Exactly what to verify (3-5 points)
- How to verify each point
- An example of a failure
See solution
Improved Item 4: User inputs are validated with Pydantic constraints
WHAT TO VERIFY:
1. Does each string have max_length defined?
→ Prevents DoS from 10MB inputs
2. Do the numbers have defined ranges? (ge, le, gt, lt)
→ Prevents negative prices, quantities of 999999999
3. Are Enums used for limited values?
→ status should be an Enum, not a free str
4. Are there inputs that arrive as dict or Any?
→ Complete bypass of validation
5. Do emails use EmailStr, not str?
→ str accepts "not-an-email" as an email
HOW TO VERIFY:
- Open each BaseModel: review each Field()
- Look for fields without constraints: str without max_length, int without ge/le
- Search for: dict, Any, str where there should be an Enum
EXAMPLE OF A FAILURE:
# No constraints
class UserCreate(BaseModel):
name: str # Can be "" or a 10MB string
age: int # Can be -50 or 999999
role: str # Can be "hacked_superadmin"
email: str # Can be "not an email"
# With constraints
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
age: int = Field(..., ge=0, le=150)
role: UserRole # Enum with valid values
email: EmailStr
Exercise 3: Find the missing item (Hard)
The 20-item checklist doesn't cover a scenario you found in your review: the AI-generated code uses datetime.utcnow() which is deprecated in Python 3.12+ (use datetime.now(timezone.utc) instead).
In which category would you add this item? Write the complete item in the checklist format.
See solution
Category: AI-Specific (alongside items 14-17)
This item goes in AI-Specific because AI trains on code that includes datetime.utcnow() extensively (it was the standard for years). AI will keep generating this pattern even after it was deprecated. An up-to-date human developer would know; AI doesn't necessarily.
Item 16.5: Doesn't use deprecated language or library APIs
WHAT TO VERIFY:
- datetime.utcnow() → deprecated in Python 3.12+
- datetime.utcfromtimestamp() → deprecated
- asyncio.get_event_loop() → changed behavior in 3.10+
- pkg_resources → replaced by importlib.resources
- unittest.TestCase with nose-style → prefer pytest
HOW TO VERIFY:
- Search for: utcnow(), utcfromtimestamp()
- Verify the project's Python version
- Check the "What's New" docs of the current version
EXAMPLE OF A FAILURE:
# Deprecated in Python 3.12+
from datetime import datetime
created_at = datetime.utcnow() # Not timezone-aware
# Correct
from datetime import datetime, timezone
created_at = datetime.now(timezone.utc) # Timezone-aware
SEVERITY IF IT FAILS: Low-Medium (it works but generates deprecation warnings,
and can cause timezone bugs)
ESTIMATED TIME: 1-2 minutes
Note: This is a perfect example of why the checklist is a living tool. Every review where something slips past you is an opportunity to add an item.
Exercise 4: Checklist for your domain (Hard)
Add 3 items to the checklist that are specific to your work domain. If you work in fintech, healthtech, e-commerce, or any other domain, there are specific checks the generic checklist doesn't cover.
See examples by domain
Fintech:
- Item F1: Do the amounts use Decimal with defined precision, never float?
- Item F2: Is there an audit trail for each transaction? (who, when, what)
- Item F3: Do the financial operations have idempotency keys?
Healthtech:
- Item H1: Is the health data encrypted at rest (HIPAA)?
- Item H2: Are there access logs for patient records?
- Item H3: Do the medical data endpoints require multi-factor authentication?
E-commerce:
- Item E1: Are the prices calculated server-side, never from the client?
- Item E2: Is the inventory checked at the moment of payment, not just when adding to the cart?
- Item E3: Do the coupons have single-use/expiration validation?
Your turn: Define 3 items for YOUR domain in the complete format (what to verify, how to verify, example of a failure).
Summary
In this capsule you built:
- A professional 20-item checklist organized into 5 categories aligned with the pyramid
- Category 1 (Security): 5 items reviewed always — secrets, SQL, auth, validation, error messages
- Category 2 (Logic): 4 items for business rules — correct problem, calculations, states, atomicity
- Category 3 (Edge Cases): 4 items for unexpected inputs — None, error handling, pagination, concurrency
- Category 4 (AI-Specific): 4 items that only apply to AI code — fake imports, over-engineering, incorrect APIs, mixing frameworks
- Category 5 (Quality): 3 items for maintainability — response models, structure, tests
- A pocket checklist for daily use
- The principle that the checklist is a living tool you improve with each review
Next capsule: Red Flags in AI Code — going deeper into items 14-17 with detailed examples and specific patterns.
Additional resources
- Google — Code Review Developer Guide - The code review standard that inspires this checklist
- OWASP — Web Security Testing Guide - An exhaustive guide for the security items
- Pydantic — Field Types and Validators - Reference for input validation (item 4)
- FastAPI — Security Best Practices - Authentication and authorization in FastAPI (item 3)
- Python — What's New in 3.12 - Deprecations relevant to AI-specific items
Debugging & Code Review with Claude Code — Module 4, Capsule 03 Claude Code Agentic Development Path — Guide #6 of 11