Module 4: Code Review of AI Output

Red Flags in AI-Generated Code

Red Flags in AI-Generated Code

Capsule overview

There are code smells that exist in all code — high cyclomatic complexity, deep nesting, 200-line functions. You already know those. This capsule focuses on red flags that only appear in AI-generated code: patterns a human developer rarely produces but that AI generates frequently.

These red flags are your specialized knowledge. Any developer knows how to detect a 5-level nested if. Few know how to detect that AI solved a slightly different problem than the one asked, or that it used an API that existed in version 1.x of a library but was removed in 2.0, or that it created a complete Factory pattern for something that needed a 3-line function.

When you master these red flags, you'll be able to review AI code in half the time because you'll know exactly where to look.


The 8 Red Flags of AI Code

Red Flag 1: Over-Engineering for Simple Problems

What it is: AI generates complex solutions for problems that require simple solutions. Design patterns, abstractions, layers of indirection — all with no justification.

Why AI does it: Language models were trained on millions of repositories containing design patterns. When you generate code, AI tends to "show" its knowledge by applying patterns it saw frequently, regardless of whether the problem needs them.

Warning signs:

⚠️ Abstract classes with a single concrete implementation
⚠️ Factory patterns for a single type of object
⚠️ Strategy pattern when there's only one strategy
⚠️ Observer/Event system for a simple linear flow
⚠️ Dependency injection containers for 3 functions
⚠️ A 50-line configuration file for a 100-line app

Real example:

Prompt: "Create an endpoint that returns the current date in different formats."

AI generates:

import abc
from datetime import datetime
from typing import Dict, Type
from fastapi import FastAPI, Query
from enum import Enum

app = FastAPI()


class DateFormat(str, Enum):
    ISO = "iso"
    US = "us"
    EU = "eu"
    UNIX = "unix"


class DateFormatter(abc.ABC):
    @abc.abstractmethod
    def format(self, dt: datetime) -> str: ...

    @abc.abstractmethod
    def get_name(self) -> str: ...


class ISOFormatter(DateFormatter):
    def format(self, dt: datetime) -> str:
        return dt.isoformat()

    def get_name(self) -> str:
        return "ISO 8601"


class USFormatter(DateFormatter):
    def format(self, dt: datetime) -> str:
        return dt.strftime("%m/%d/%Y %I:%M %p")

    def get_name(self) -> str:
        return "US Format"


class EUFormatter(DateFormatter):
    def format(self, dt: datetime) -> str:
        return dt.strftime("%d/%m/%Y %H:%M")

    def get_name(self) -> str:
        return "EU Format"


class UnixFormatter(DateFormatter):
    def format(self, dt: datetime) -> str:
        return str(int(dt.timestamp()))

    def get_name(self) -> str:
        return "Unix Timestamp"


class FormatterRegistry:
    _formatters: Dict[DateFormat, Type[DateFormatter]] = {}

    @classmethod
    def register(cls, fmt: DateFormat, formatter_cls: Type[DateFormatter]):
        cls._formatters[fmt] = formatter_cls

    @classmethod
    def get(cls, fmt: DateFormat) -> DateFormatter:
        formatter_cls = cls._formatters.get(fmt)
        if not formatter_cls:
            raise ValueError(f"Unknown format: {fmt}")
        return formatter_cls()


FormatterRegistry.register(DateFormat.ISO, ISOFormatter)
FormatterRegistry.register(DateFormat.US, USFormatter)
FormatterRegistry.register(DateFormat.EU, EUFormatter)
FormatterRegistry.register(DateFormat.UNIX, UnixFormatter)


@app.get("/date")
async def get_date(fmt: DateFormat = Query(default=DateFormat.ISO)):
    formatter = FormatterRegistry.get(fmt)
    now = datetime.utcnow()
    return {
        "format_name": formatter.get_name(),
        "formatted": formatter.format(now),
    }

What you should have:

from datetime import datetime, timezone
from fastapi import FastAPI, Query
from enum import Enum

app = FastAPI()


class DateFormat(str, Enum):
    ISO = "iso"
    US = "us"
    EU = "eu"
    UNIX = "unix"


FORMATS = {
    DateFormat.ISO: lambda dt: dt.isoformat(),
    DateFormat.US: lambda dt: dt.strftime("%m/%d/%Y %I:%M %p"),
    DateFormat.EU: lambda dt: dt.strftime("%d/%m/%Y %H:%M"),
    DateFormat.UNIX: lambda dt: str(int(dt.timestamp())),
}


@app.get("/date")
async def get_date(fmt: DateFormat = Query(default=DateFormat.ISO)):
    now = datetime.now(timezone.utc)
    return {"formatted": FORMATS[fmt](now)}

80 lines → 25 lines. Same functionality. No unnecessary abstractions.

Detection rule: If the number of classes or files is greater than the number of features, there's over-engineering.


Red Flag 2: APIs from Earlier Versions

What it is: AI uses functions, parameters, or patterns from earlier versions of libraries. The code works (sometimes) but uses deprecated APIs, removed ones, or ones with changed behavior.

Why AI does it: Models are trained on code that existed at the time of the training cutoff. If a library changed its API after that date, AI keeps using the earlier version. Even with updated data, AI can mix patterns from different versions because it saw millions of examples of each one.

Warning signs:

⚠️ Deprecation warnings when running
⚠️ Function names that "sound" like the library but don't exist
⚠️ Parameters with names slightly different from the current ones
⚠️ Usage patterns that don't match the current documentation
⚠️ .decode("utf-8") in pyjwt >= 2.0 (it already returns str)
⚠️ datetime.utcnow() in Python 3.12+ (deprecated)

Real example:

# AI generates code for Pydantic v1 in a project with Pydantic v2
from pydantic import BaseModel, validator

class UserCreate(BaseModel):
    name: str
    email: str

    @validator("email")
    def validate_email(cls, v):
        if "@" not in v:
            raise ValueError("Invalid email")
        return v

    class Config:
        orm_mode = True

Problems:

❌ @validator → deprecated in v2, use @field_validator
❌ cls as the first parameter → in v2 it's @classmethod + cls
❌ class Config → deprecated in v2, use model_config = ConfigDict(...)
❌ orm_mode → renamed to from_attributes in v2

Correct version for Pydantic v2:

from pydantic import BaseModel, ConfigDict, field_validator


class UserCreate(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    name: str
    email: str

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Invalid email")
        return v

Detection rule: If the code doesn't match the first results of the current official documentation, it's probably using an earlier version.


Red Flag 3: Abstractions Nobody Asked For

What it is: AI adds layers of abstraction, services, repositories, or modules you didn't ask for and that the problem doesn't need. Different from over-engineering (Red Flag 1): here it isn't an unnecessary pattern, but complete functionality you didn't request.

Why AI does it: AI tries to be "helpful" by anticipating future needs. It saw thousands of repositories where a simple endpoint evolves into a service layer → repository layer → DTO layer. It generates the whole evolution at once even though you only asked for the endpoint.

Warning signs:

⚠️ More files than you asked for
⚠️ Service layers for 5-line logic
⚠️ Repository pattern when there's no ORM
⚠️ Separate DTOs when the Pydantic model is enough
⚠️ Event handlers for flows that have no events
⚠️ Custom middleware for functionality FastAPI already has

Real example:

Prompt: "Create an endpoint to save and get notes."

AI generates 5 files:

app/
├── models/
│   └── note.py          # Note model
├── schemas/
│   ├── note_create.py   # NoteCreate schema
│   ├── note_update.py   # NoteUpdate schema
│   └── note_response.py # NoteResponse schema
├── repositories/
│   └── note_repository.py  # NoteRepository with interface
├── services/
│   └── note_service.py     # NoteService that calls the repository
└── routes/
    └── notes.py             # Router that calls the service

For two endpoints (POST and GET) with data saved in a dict, this is excessive. The correct answer is a single file with 30 lines.

Detection rule: If you struggle to find where the real logic is because there are too many layers of indirection, there are abstractions nobody asked for.


Red Flag 4: Code That Solves a Slightly Different Problem

What it is: The generated code is correct, professional, and functional — but it solves a different problem than the one you asked for. The difference is subtle: an extra field, a slightly different calculation, a flow with an additional or missing step.

Why AI does it: AI generates code based on patterns. If your problem resembles the most common pattern, AI generates the pattern, not your problem. It's like asking "how do I get to my house?" and getting directions to the house closest to the question's pattern.

Warning signs:

⚠️ The code looks correct but something "feels different"
⚠️ There are fields or parameters you didn't ask for
⚠️ Fields or parameters you did ask for are missing
⚠️ The flow has steps that aren't in your requirements
⚠️ The response includes data you didn't expect
⚠️ The approach solves the general case instead of your specific case

Real example:

Prompt: "Endpoint for an admin to deactivate user accounts. The deactivated user can't log in but their data is preserved."

AI generates:

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from datetime import datetime
from typing import Optional

app = FastAPI()


class DeactivateRequest(BaseModel):
    reason: str
    notify_user: bool = True
    delete_after_days: Optional[int] = 30


@app.post("/admin/users/{user_id}/deactivate")
async def deactivate_user(
    user_id: str,
    request: DeactivateRequest,
    admin: dict = Depends(get_current_admin),
):
    user = get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    user.is_active = False
    user.deactivated_at = datetime.utcnow()
    user.deactivation_reason = request.reason
    user.scheduled_deletion = datetime.utcnow() + timedelta(
        days=request.delete_after_days
    )
    save_user(user)

    if request.notify_user:
        send_deactivation_email(user.email, request.reason)

    schedule_data_deletion(user_id, request.delete_after_days)

    return {"status": "deactivated", "user_id": user_id}

Problems (differences from what was asked):

⚠️ delete_after_days: "their data is preserved" — you didn't ask for 
   scheduled deletion. AI assumed a GDPR flow you didn't ask for.
   
⚠️ schedule_data_deletion: a function that schedules deletion. 
   Directly contradicts "their data is preserved."
   
⚠️ notify_user + send_deactivation_email: you didn't ask for 
   notification. It can be useful but it's NOT what you asked for.
   
⚠️ deactivation_reason: you didn't ask to record a reason. 
   An extra field that wasn't in the requirements.
   
✅ is_active = False: correct
✅ Can't log in: correct (if the login checks is_active)

The code is professional and well written. But it solves "deactivate user with GDPR compliance" instead of "deactivate simple user."

Detection rule: Compare your prompt/requirement with the code line by line. Every line of code that doesn't map to a requirement is suspicious.


Red Flag 5: Confidence Without Correctness

What it is: The code looks extremely professional — good structure, impeccable naming, type hints, docstrings — but the underlying logic is incorrect. The presentation generates confidence the substance doesn't deserve.

Why AI does it: AI is excellent at form and can fail at substance. It produces code that looks like it was written by a senior developer: well formatted, with comments, with tests. But the calculations may be wrong, the logic may be incorrect, and the tests may verify the incorrect behavior.

Warning signs:

⚠️ Code with detailed docstrings but incorrect simple logic
⚠️ Tests that pass but verify the incorrect result
⚠️ Comments that explain one thing but the code does another
⚠️ Descriptive names that don't match the behavior
⚠️ Professional error handling for errors that won't occur,
   while ignoring real errors

Real example:

from decimal import Decimal
from typing import List
from pydantic import BaseModel


class TaxCalculator:
    """Calculates progressive tax based on income brackets.
    
    Uses the standard progressive tax system where each bracket
    is taxed at its corresponding rate. Only the income within
    each bracket is taxed at that bracket's rate.
    """

    BRACKETS = [
        (Decimal("10000"), Decimal("0.10")),
        (Decimal("40000"), Decimal("0.20")),
        (Decimal("85000"), Decimal("0.30")),
        (Decimal("999999999"), Decimal("0.35")),
    ]

    def calculate(self, income: Decimal) -> Decimal:
        """Calculate total tax for given income using progressive brackets."""
        if income <= 0:
            return Decimal("0")

        total_tax = Decimal("0")
        for bracket_limit, rate in self.BRACKETS:
            if income <= bracket_limit:
                total_tax += income * rate
                break
            total_tax += bracket_limit * rate
            income -= bracket_limit

        return total_tax.quantize(Decimal("0.01"))

The subtle problem:

The docstring says "progressive tax" and the structure suggests progressive brackets. But the calculation has an error: income -= bracket_limit modifies income inside the loop, which affects the if income <= bracket_limit comparison for the next bracket.

For an income of $50,000:

  • Bracket 1: $10,000 × 10% = $1,000. income is reduced to $40,000.
  • Bracket 2: income ($40,000) ≤ $40,000 → $40,000 × 20% = $8,000. Break.
  • Total: $9,000.

Is that correct? It depends: if the brackets are cumulative ($0-10k, $10k-50k, $50k-135k), then yes. If the brackets are absolute ($0-10k, $0-40k, $0-85k), then no. The code doesn't make it clear which, and the docstring doesn't specify it.

The code looks impeccable. It has a docstring, type hints, Decimal for precision, handling of income ≤ 0, and quantize for rounding. But the logic could be wrong, and the professional presentation generates false confidence.

Detection rule: The more professional the code looks, the more attention you should pay to the logic. The presentation is inversely correlated with your level of suspicion — and it should be the other way around.


Red Flag 6: Mixing Patterns from Different Frameworks

What it is: AI combines patterns, imports, or conventions from different frameworks in the same code. Mixing Flask with FastAPI, Django ORM with SQLAlchemy, or Express.js patterns in a Python app.

Why AI does it: AI saw millions of examples of all the frameworks. When it generates code for FastAPI, it can be "contaminated" with Flask or Django patterns that resemble it. The patterns look similar superficially but have important differences in execution.

Warning signs:

⚠️ Imports from frameworks you don't use in the project
⚠️ Decorators from the wrong framework (@app.route vs @app.get)
⚠️ Return types from the wrong framework (jsonify in FastAPI)
⚠️ Middleware patterns from one framework in another
⚠️ Configuration with the style of a different framework
⚠️ Synchronous functions where they should be async (or vice versa)

Real example:

from fastapi import FastAPI
from flask import jsonify, request  # Flask in a FastAPI project?

app = FastAPI()


@app.route("/users", methods=["GET"])  # @app.route is Flask, not FastAPI
def get_users():  # No async
    page = request.args.get("page", 1, type=int)  # request.args is Flask
    users = User.query.all()  # .query.all() is Flask-SQLAlchemy
    return jsonify([u.to_dict() for u in users])  # jsonify is Flask

Correct version (pure FastAPI):

from fastapi import FastAPI, Query
from typing import List

app = FastAPI()


@app.get("/users", response_model=List[UserResponse])
async def get_users(page: int = Query(default=1, ge=1)):
    users = await db.fetch_users(page=page)
    return users

Detection rule: If you see an import that isn't from the project's framework, stop immediately. Then verify that all the patterns are from the correct framework.


Red Flag 7: Tests That Confirm the Code, Not the Requirements

What it is: AI generates tests that verify that the code does what the code does — not that it does what it should do. The tests are tautological: they always pass because they verify the actual output, not the correct output.

Why AI does it: AI generates tests based on the code it just wrote. If the code miscalculates a discount, the test verifies the miscalculation. Both are consistent with each other but inconsistent with reality.

Warning signs:

⚠️ All tests pass on the first try (suspicious — real tests 
   usually fail at least once during development)
⚠️ The assertions use values that look "calculated" instead of 
   "expected by the business"
⚠️ There are no tests for edge cases or errors
⚠️ The tests only verify the status code, not the response body
⚠️ The tests have no description of which scenario they verify

Real example:

def test_calculate_discount():
    result = calculate_discount(Decimal("100"), "SAVE10")
    assert result == Decimal("90.00")  # How do they know 90 is correct?
    # If the discount should be $10 (not 10%), 
    # then 90 is correct.
    # If it should be 10% of the subtotal and the subtotal is 100,
    # then 90 is correct.
    # But if the "SAVE10" discount is $10 off 
    # and the subtotal already includes tax, is 90 correct?


def test_calculate_tax():
    result = calculate_tax(Decimal("100"), "CA")
    assert result == Decimal("7.25")
    # Is 7.25% correct for California? In which year?
    # California state tax is 7.25%, but counties add more.
    # Does the test verify the correct rate or the rate AI hardcoded?

Detection rule: For each assertion, ask yourself: "can I derive this expected value from the business requirements, without looking at the code?" If the answer is no, the test could be confirming a bug instead of verifying correct functionality.


Red Flag 8: Cosmetic Error Handling

What it is: AI generates try/except blocks that look professional but actually hide errors, catch too broadly, or handle the wrong errors while ignoring the real ones.

Why AI does it: AI knows error handling is "good practice", so it adds it. But it doesn't always know WHICH errors are likely or HOW to handle them correctly. The result is error handling that looks good but doesn't work.

Warning signs:

⚠️ except Exception: a catch-all that hides bugs
⚠️ try/except with pass (silences errors)
⚠️ Error handling for impossible errors but not for likely ones
⚠️ Retry logic without backoff or without a limit
⚠️ Logging the error but no real handling
⚠️ HTTPException(500) for every type of error

Real example:

@app.post("/process")
async def process_data(data: ProcessRequest):
    try:
        result = await complex_processing(data)
        await save_to_database(result)
        await notify_webhook(result)
        return {"status": "success", "result": result}
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid data")
    except ConnectionError:
        raise HTTPException(status_code=503, detail="Service unavailable")
    except Exception:
        raise HTTPException(status_code=500, detail="Internal error")

The problem:

⚠️ If save_to_database fails with a ConnectionError, 
   the processing was already done but wasn't saved.
   The user gets 503 and retries → duplicate processing.
   
⚠️ If notify_webhook fails, 
   the result was already saved in the DB.
   The user gets an error but the data was saved.
   
⚠️ except Exception hides ALL the other errors.
   TypeError? "Internal error." 
   ImportError? "Internal error."
   You won't know what happened.

Detection rule: For each except block, ask yourself: "what state is the system left in if this error occurs?" If you can't answer, the error handling is cosmetic.


Summary Table: The 8 Red Flags

┌─────┬────────────────────────────────────┬──────────┬──────────────────┐
│  #  │ Red Flag                           │ Risk     │ Detection        │
├─────┼────────────────────────────────────┼──────────┼──────────────────┤
│  1  │ Over-engineering                   │ Medium   │ Count classes    │
│  2  │ APIs from earlier versions         │ High     │ Verify docs      │
│  3  │ Unrequested abstractions           │ Medium   │ Count files      │
│  4  │ Slightly different problem         │ High     │ Compare prompt   │
│  5  │ Confidence without correctness     │ Critical │ Verify logic     │
│  6  │ Mixing frameworks                  │ High     │ Review imports   │
│  7  │ Tautological tests                 │ High     │ Derive values    │
│  8  │ Cosmetic error handling            │ High     │ Trace states     │
└─────┴────────────────────────────────────┴──────────┴──────────────────┘

Connection to the Project

Red flags in the capstone project (Module 8)

The codebase of the capstone project contains at least 3-4 of these red flags planted intentionally. Not all the project's problems are "bugs" — some are red flags that don't cause immediate errors but indicate quality or maintainability problems.

Your ability to detect these red flags is what separates a superficial code review ("it compiles, the tests pass, I approve") from a professional code review ("it compiles and the tests pass, but there are 3 red flags that need attention").


Troubleshooting

Problem 1: "I'm not sure whether it's over-engineering or good design"

Cause: The line between "prepared for the future" and "over-engineering" is blurry. Solution: Ask yourself: "Do I need this abstraction TODAY to solve the current problem?" If the answer is no, it's over-engineering. YAGNI (You Ain't Gonna Need It). If tomorrow you need the abstraction, you add it tomorrow in 15 minutes. You don't build it today for a future that probably doesn't arrive.

Problem 2: "I don't know all the versions of the libraries"

Cause: Nobody knows them all. You don't need to memorize them. Solution: The rule is simple: if an import or function raises doubt, verify. Open the library's official documentation for the version you use. 2 minutes of verification saves you 2 hours of debugging in production. Over time, you'll internalize the most common changes (Pydantic v1 → v2, pyjwt < 2.0 → >= 2.0, etc.).

Problem 3: "I can't tell whether the code solves my problem or a different one"

Cause: Your prompt/requirement isn't specific enough. Solution: Before the review, write in 2-3 sentences what the code should do. Then compare each function with those sentences. If there are functions that don't map to any sentence, they're suspicious. If there are sentences that don't map to any function, something is missing.

Problem 4: "The tests pass — do I really need to be suspicious?"

Cause: Passing tests generate false confidence. Solution: Don't ask "do the tests pass?" Ask "what do the tests verify?" A test that verifies assert response.status_code == 200 tells you nothing about whether the data is correct. Read the assertions, not the results.

Problem 5: "My team doesn't understand why I flag AI red flags — the code works"

Cause: AI red flags aren't immediate bugs — they're indicators of future problems. Solution: Explain it like this: "The code works today. But it uses a deprecated API that will stop working in the next version. Better to fix it now in 5 minutes than to debug it in production when it breaks." Red flags are preventive maintenance, not emergency repair.


Exercises

Exercise 1: Identify the red flag (Easy)

For each snippet, identify which of the 8 red flags is present:

Snippet A:

from fastapi import FastAPI
from django.db import models  # ???

app = FastAPI()

class User(models.Model):
    name = models.CharField(max_length=100)

Snippet B:

class NotificationService(abc.ABC):
    @abc.abstractmethod
    def send(self, message: str): ...

class EmailNotification(NotificationService):
    def send(self, message: str):
        send_email(message)

# There's only one implementation. No SMS, Push, or Slack.

Snippet C:

def test_user_age():
    user = create_user(birth_year=1990)
    assert user.age == 34  # Hardcoded — and in 2026?
See solution

Snippet A → Red Flag 6: Mixing frameworks. Django models in a FastAPI project. Django ORM and FastAPI aren't directly compatible.

Snippet B → Red Flag 3: Unrequested abstractions (and partially Red Flag 1: over-engineering). An abstract class with a single implementation is a premature abstraction. If tomorrow you need Slack notifications, you add it in 5 minutes. Today, the abstraction adds complexity without value.

Snippet C → Red Flag 7: Tautological tests. The test hardcodes 34 as the expected age. In 2025 it's correct, in 2026 it fails. The test verifies a value calculated by the developer at the time of writing the test, not a value derived from the business logic. It should be: assert user.age == datetime.now().year - 1990.

Exercise 2: Red flag hunting (Medium)

This code was generated by Claude Code. Find all the red flags (there are at least 4):

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, validator
from typing import Optional, List
from datetime import datetime
import abc
import json

app = FastAPI()

class TaskPriority(int):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

class BaseRepository(abc.ABC):
    @abc.abstractmethod
    def save(self, entity): ...
    
    @abc.abstractmethod
    def find(self, id): ...
    
    @abc.abstractmethod
    def delete(self, id): ...

class TaskRepository(BaseRepository):
    def __init__(self):
        self.tasks = {}
    
    def save(self, task):
        self.tasks[task["id"]] = task
    
    def find(self, id):
        return self.tasks.get(id)
    
    def delete(self, id):
        if id in self.tasks:
            del self.tasks[id]

class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = None
    
    @validator("title")
    def title_not_empty(cls, v):
        if not v.strip():
            raise ValueError("Title cannot be empty")
        return v
    
    class Config:
        orm_mode = True

repo = TaskRepository()

@app.post("/tasks")
async def create_task(task: TaskCreate):
    try:
        new_task = {
            "id": str(len(repo.tasks) + 1),
            "title": task.title,
            "description": task.description,
            "created_at": datetime.utcnow().isoformat(),
        }
        repo.save(new_task)
        return new_task
    except Exception:
        raise HTTPException(status_code=500, detail="Error creating task")

@app.get("/tasks/{task_id}")
async def get_task(task_id: str):
    task = repo.find(task_id)
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return task
See solution

Red Flag 1: Over-engineering (BaseRepository). An ABC with 3 abstract methods for an in-memory dict. The TaskRepository implementation adds no value over the direct dict. For a CRUD with a single type of entity, this is unnecessary.

Red Flag 2: API from an earlier version (Pydantic v1). @validator (v1) instead of @field_validator (v2). class Config: orm_mode = True (v1) instead of model_config = ConfigDict(from_attributes=True) (v2). If the project uses Pydantic v2, this code doesn't work correctly.

Red Flag 3: Unrequested abstractions. For a "create a tasks endpoint", AI generated a Repository pattern with an abstract interface. All that was needed was a dict and two functions.

Red Flag 8: Cosmetic error handling. except Exception in the POST catches EVERYTHING — including TypeError, KeyError, or any bug in the logic. They all become "Error creating task" 500. A real bug is hidden behind a generic message.

Bonus — Not an AI red flag but a bug:

  • id: str(len(repo.tasks) + 1) — IDs get reused if you delete tasks.
  • TaskPriority(int) isn't an Enum — it's a class that inherits from int. TaskPriority.LOW doesn't exist as expected.
  • datetime.utcnow() — deprecated in Python 3.12+.

Exercise 3: Rewrite without red flags (Medium)

Take the code from Exercise 2 and rewrite it removing all the red flags. Keep the same functionality.

See solution
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Optional
from datetime import datetime, timezone
import uuid

app = FastAPI()

tasks_db: dict = {}


class TaskCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    description: Optional[str] = Field(None, max_length=2000)

    @field_validator("title")
    @classmethod
    def title_not_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Title cannot be empty")
        return v.strip()


class TaskResponse(TaskCreate):
    id: str
    created_at: datetime


@app.post("/tasks", response_model=TaskResponse, status_code=201)
async def create_task(task: TaskCreate):
    task_id = str(uuid.uuid4())
    new_task = TaskResponse(
        id=task_id,
        created_at=datetime.now(timezone.utc),
        **task.model_dump(),
    )
    tasks_db[task_id] = new_task
    return new_task


@app.get("/tasks/{task_id}", response_model=TaskResponse)
async def get_task(task_id: str):
    task = tasks_db.get(task_id)
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return task

Changes:

  • ❌ Removed BaseRepository and TaskRepository → direct dict
  • ❌ Removed the useless TaskPriority class
  • ✅ Pydantic v2: @field_validator + @classmethod
  • ✅ UUID instead of len()+1 for IDs
  • ✅ datetime.now(timezone.utc) instead of utcnow()
  • ✅ No except Exception catch-all
  • ✅ response_model and status_code=201
  • ✅ 40 lines instead of 70+ — same functionality

Exercise 4: Create your red flags catalog (Hard)

Generate 3 pieces of code with Claude Code (or your AI tool) and document the red flags you find. For each one:

  1. The prompt you used
  2. The generated code (relevant part)
  3. The identified red flags (which of the 8)
  4. The severity
  5. The fix
See evaluation guide

Your catalog should:

  • ✅ Have at least 3 different generations (not the same prompt 3 times)
  • ✅ Each generation with at least 1 red flag identified
  • ✅ Red flags classified by number (1-8)
  • ✅ Justified severity
  • ✅ A concrete fix (not "fix it")

If you don't find red flags in 3 generations, your prompt is too simple. Try: services that integrate external APIs, business logic with specific rules, or custom middleware.


Summary

In this capsule you learned:

  • The 8 red flags specific to AI code that don't exist in human code
  • Red Flag 1 (Over-engineering): patterns for problems that don't need them
  • Red Flag 2 (Earlier APIs): deprecated functions or ones from old versions
  • Red Flag 3 (Phantom abstractions): layers of indirection nobody asked for
  • Red Flag 4 (Different problem): code that solves something similar but not identical
  • Red Flag 5 (Confidence without correctness): professional code with incorrect logic
  • Red Flag 6 (Mixing frameworks): patterns from one framework in another
  • Red Flag 7 (Tautological tests): tests that confirm the code, not the requirements
  • Red Flag 8 (Cosmetic error handling): try/except that looks good but hides bugs
  • Quick detection rules for each red flag
  • These red flags are specialized knowledge that sets you apart from developers who only review human code

Next capsule: Verify Business Logic — the hardest and most important part of AI code review.


Additional resources

  1. Martin Fowler — Refactoring: Improving the Design of Existing Code - Reference for distinguishing between good design and over-engineering
  2. YAGNI — You Aren't Gonna Need It - A principle that applies directly to Red Flags 1 and 3
  3. Pydantic — Migration Guide v1 to v2 - Reference for detecting Red Flag 2 in Pydantic
  4. PyJWT — Changelog - API changes AI doesn't always reflect
  5. FastAPI vs Flask — Key Differences - Reference for detecting Red Flag 6
  6. Google — Writing Clean Tests - Principles for detecting Red Flag 7

Debugging & Code Review with Claude Code — Module 4, Capsule 04 Claude Code Agentic Development Path — Guide #6 of 11