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 itemTypical 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
#ItemResultNote
1Hardcoded secrets✅No secrets
2SQL parameterizedN/ANo SQL
3Auth on endpoints⚠️POST with no auth — should it have any?
4Inputs validated⚠️No min_length, price can be negative
5Errors don't expose info✅Only "Product not found"
6Solves the problem asked✅Correct basic CRUD
7Correct calculationsN/ANo calculations
8State transitionsN/ANo states
9Atomic operationsN/ASingle-step operations
10Handles None/empty✅Optional category works with None
11Error handling⚠️Only 404, no handling for other errors
12Pagination❌No pagination — returns EVERYTHING
13Race conditionsN/AIn-memory, single process
14Imports exist✅They're all real imports
15Over-engineering✅Simple and proportional
16Correct APIs✅FastAPI and Pydantic used correctly
17Mixes frameworks✅Only FastAPI
18Response filters data✅ProductResponse has no sensitive data
19Structure conventions✅Standard structure
20TestsN/ANo tests

Summary: 2 main issues:

  1. price: float should be Decimal with gt=0 — accepts negatives and has imprecision
  2. 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

  1. Google — Code Review Developer Guide - The code review standard that inspires this checklist
  2. OWASP — Web Security Testing Guide - An exhaustive guide for the security items
  3. Pydantic — Field Types and Validators - Reference for input validation (item 4)
  4. FastAPI — Security Best Practices - Authentication and authorization in FastAPI (item 3)
  5. 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