Módulo 4: Code Review de Output AI
Red Flags en Código Generado por AI
Red Flags en Código Generado por AI
Descripción de la cápsula
Hay code smells que existen en todo código — complejidad ciclomática alta, deep nesting, funciones de 200 líneas. Esos ya los conoces. Esta cápsula se enfoca en red flags que solo aparecen en código generado por AI: patrones que un developer humano rara vez produce pero que AI genera con frecuencia.
Estos red flags son tu conocimiento especializado. Cualquier developer sabe detectar un if anidado 5 niveles. Pocos saben detectar que AI resolvió un problema ligeramente diferente al pedido, o que usó una API que existía en la versión 1.x de una librería pero fue removida en la 2.0, o que creó un Factory pattern completo para algo que necesitaba una función de 3 líneas.
Cuando domines estos red flags, podrás revisar código AI en la mitad del tiempo porque sabrás exactamente dónde mirar.
Los 8 Red Flags de Código AI
Red Flag 1: Sobre-ingeniería para Problemas Simples
Qué es: AI genera soluciones complejas para problemas que requieren soluciones simples. Patterns de diseño, abstracciones, capas de indirección — todo sin justificación.
Por qué AI lo hace: Los modelos de lenguaje fueron entrenados con millones de repositorios que contienen patterns de diseño. Cuando generas código, AI tiende a "demostrar" su conocimiento aplicando patterns que vio frecuentemente, independientemente de si el problema los necesita.
Señales de alarma:
⚠️ Clases abstractas con una sola implementación concreta
⚠️ Factory patterns para un solo tipo de objeto
⚠️ Strategy pattern cuando solo hay una estrategia
⚠️ Observer/Event system para un flujo lineal simple
⚠️ Dependency injection containers para 3 funciones
⚠️ Archivo de configuración de 50 líneas para una app de 100 líneas
Ejemplo real:
Prompt: "Crea un endpoint que devuelva la fecha actual en diferentes formatos."
AI genera:
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),
}
Lo que deberías tener:
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 líneas → 25 líneas. Misma funcionalidad. Sin abstracciones innecesarias.
Regla de detección: Si el número de clases o archivos es mayor al número de funcionalidades, hay sobre-ingeniería.
Red Flag 2: APIs de Versiones Anteriores
Qué es: AI usa funciones, parámetros, o patrones de versiones anteriores de librerías. El código funciona (a veces) pero usa APIs deprecated, removidas, o con comportamiento cambiado.
Por qué AI lo hace: Los modelos se entrenan con código que existía en el momento del training cutoff. Si una librería cambió su API después de esa fecha, AI sigue usando la versión anterior. Incluso con datos actualizados, AI puede mezclar patrones de diferentes versiones porque vio millones de ejemplos de cada una.
Señales de alarma:
⚠️ Deprecation warnings al ejecutar
⚠️ Nombres de funciones que "suenan" a la librería pero no existen
⚠️ Parámetros con nombres ligeramente diferentes a los actuales
⚠️ Patrones de uso que no coinciden con la documentación actual
⚠️ .decode("utf-8") en pyjwt >= 2.0 (ya devuelve str)
⚠️ datetime.utcnow() en Python 3.12+ (deprecated)
Ejemplo real:
# AI genera código para Pydantic v1 en un proyecto con 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
Problemas:
❌ @validator → deprecated en v2, usar @field_validator
❌ cls como primer parámetro → en v2 es @classmethod + cls
❌ class Config → deprecated en v2, usar model_config = ConfigDict(...)
❌ orm_mode → renamed a from_attributes en v2
Versión correcta para 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
Regla de detección: Si el código no coincide con los primeros resultados de la documentación oficial actual, probablemente usa una versión anterior.
Red Flag 3: Abstracciones que Nadie Pidió
Qué es: AI agrega capas de abstracción, servicios, repositorios, o módulos que no pediste y que el problema no necesita. Diferente de sobre-ingeniería (Red Flag 1): aquí no es un pattern innecesario, sino funcionalidad completa que no solicitaste.
Por qué AI lo hace: AI intenta ser "útil" anticipando necesidades futuras. Vio miles de repositorios donde un endpoint simple evoluciona a un service layer → repository layer → DTO layer. Genera toda la evolución de golpe aunque solo pediste el endpoint.
Señales de alarma:
⚠️ Más archivos de los que pediste
⚠️ Capas de servicio para lógica de 5 líneas
⚠️ Repository pattern cuando no hay ORM
⚠️ DTOs separados cuando el modelo Pydantic es suficiente
⚠️ Event handlers para flujos que no tienen eventos
⚠️ Middleware custom para funcionalidad que FastAPI ya tiene
Ejemplo real:
Prompt: "Crea un endpoint para guardar y obtener notas."
AI genera 5 archivos:
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 con interface
├── services/
│ └── note_service.py # NoteService que llama al repository
└── routes/
└── notes.py # Router que llama al service
Para dos endpoints (POST y GET) con datos guardados en un dict, esto es excesivo. La respuesta correcta es un solo archivo con 30 líneas.
Regla de detección: Si te cuesta encontrar dónde está la lógica real porque hay demasiadas capas de indirección, hay abstracciones que nadie pidió.
Red Flag 4: Código que Resuelve un Problema Ligeramente Diferente
Qué es: El código generado es correcto, profesional, y funcional — pero resuelve un problema diferente al que pediste. La diferencia es sutil: un campo extra, un cálculo ligeramente diferente, un flujo con un paso adicional o faltante.
Por qué AI lo hace: AI genera código basado en patrones. Si tu problema se parece al patrón más común, AI genera el patrón, no tu problema. Es como preguntar "¿cómo llego a mi casa?" y recibir indicaciones a la casa más cercana al patrón de la pregunta.
Señales de alarma:
⚠️ El código se ve correcto pero algo "se siente diferente"
⚠️ Hay campos o parámetros que no pediste
⚠️ Faltan campos o parámetros que sí pediste
⚠️ El flujo tiene pasos que no están en tus requisitos
⚠️ La respuesta incluye datos que no esperabas
⚠️ El approach resuelve el caso general en vez de tu caso específico
Ejemplo real:
Prompt: "Endpoint para que un admin pueda desactivar cuentas de usuario. El usuario desactivado no puede hacer login pero sus datos se conservan."
AI genera:
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}
Problemas (diferencias con lo pedido):
⚠️ delete_after_days: "sus datos se conservan" — no pidiste
borrado programado. AI asumió un flujo GDPR que no pediste.
⚠️ schedule_data_deletion: función que programa borrado.
Directamente contradice "sus datos se conservan."
⚠️ notify_user + send_deactivation_email: no pediste
notificación. Puede ser útil pero NO es lo que pediste.
⚠️ deactivation_reason: no pediste registrar razón.
Campo extra que no estaba en requisitos.
✅ is_active = False: correcto
✅ No puede hacer login: correcto (si el login verifica is_active)
El código es profesional y bien escrito. Pero resuelve "desactivar usuario con GDPR compliance" en vez de "desactivar usuario simple."
Regla de detección: Compara tu prompt/requisito con el código línea por línea. Cada línea de código que no mapea a un requisito es sospechosa.
Red Flag 5: Confianza Sin Correctitud
Qué es: El código se ve extremadamente profesional — buena estructura, naming impecable, type hints, docstrings — pero la lógica subyacente es incorrecta. La presentación genera confianza que la sustancia no merece.
Por qué AI lo hace: AI es excelente en la forma y puede fallar en el fondo. Produce código que parece escrito por un senior developer: bien formateado, con comentarios, con tests. Pero los cálculos pueden estar mal, la lógica puede ser incorrecta, y los tests pueden verificar el comportamiento incorrecto.
Señales de alarma:
⚠️ Código con docstrings detallados pero lógica simple incorrecta
⚠️ Tests que pasan pero verifican el resultado incorrecto
⚠️ Comentarios que explican una cosa pero el código hace otra
⚠️ Nombres descriptivos que no coinciden con el comportamiento
⚠️ Handling de errores profesional para errores que no van a ocurrir,
mientras ignora errores reales
Ejemplo real:
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"))
El problema sutil:
El docstring dice "progressive tax" y la estructura sugiere brackets progresivos. Pero el cálculo tiene un error: income -= bracket_limit modifica income dentro del loop, lo que afecta la comparación if income <= bracket_limit del siguiente bracket.
Para un income de $50,000:
- Bracket 1: $10,000 × 10% = $1,000. income se reduce a $40,000.
- Bracket 2: income ($40,000) ≤ $40,000 → $40,000 × 20% = $8,000. Break.
- Total: $9,000.
¿Es correcto? Depende: si los brackets son acumulativos ($0-10k, $10k-50k, $50k-135k), entonces sí. Si los brackets son absolutos ($0-10k, $0-40k, $0-85k), entonces no. El código no deja claro cuál es, y el docstring no lo especifica.
El código se ve impecable. Tiene docstring, type hints, Decimal para precisión, manejo de income ≤ 0, y quantize para redondeo. Pero la lógica podría estar mal, y la presentación profesional genera falsa confianza.
Regla de detección: Cuanto más profesional se ve el código, más atención debes poner en la lógica. La presentación es inversamente correlacionada con tu nivel de sospecha — y debería ser al revés.
Red Flag 6: Mezcla de Patrones de Diferentes Frameworks
Qué es: AI combina patrones, imports, o convenciones de frameworks diferentes en el mismo código. Mezclar Flask con FastAPI, Django ORM con SQLAlchemy, o patrones de Express.js en una app Python.
Por qué AI lo hace: AI vio millones de ejemplos de todos los frameworks. Cuando genera código para FastAPI, puede "contaminarse" con patrones de Flask o Django que se parecen. Los patrones se ven similares superficialmente pero tienen diferencias importantes en ejecución.
Señales de alarma:
⚠️ Imports de frameworks que no usas en el proyecto
⚠️ Decoradores del framework equivocado (@app.route vs @app.get)
⚠️ Return types del framework equivocado (jsonify en FastAPI)
⚠️ Patrones de middleware de un framework en otro
⚠️ Configuración con estilo de un framework diferente
⚠️ Funciones síncronas donde deberían ser async (o viceversa)
Ejemplo real:
from fastapi import FastAPI
from flask import jsonify, request # ¿Flask en un proyecto FastAPI?
app = FastAPI()
@app.route("/users", methods=["GET"]) # @app.route es Flask, no FastAPI
def get_users(): # Sin async
page = request.args.get("page", 1, type=int) # request.args es Flask
users = User.query.all() # .query.all() es Flask-SQLAlchemy
return jsonify([u.to_dict() for u in users]) # jsonify es Flask
Versión correcta (FastAPI puro):
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
Regla de detección: Si ves un import que no es del framework del proyecto, detente inmediatamente. Luego verifica que todos los patrones son del framework correcto.
Red Flag 7: Tests que Confirman el Código, No los Requisitos
Qué es: AI genera tests que verifican que el código hace lo que el código hace — no que hace lo que debería hacer. Los tests son tautológicos: pasan siempre porque verifican el output actual, no el output correcto.
Por qué AI lo hace: AI genera tests basándose en el código que acaba de escribir. Si el código calcula mal un descuento, el test verifica el cálculo mal hecho. Ambos son consistentes entre sí pero inconsistentes con la realidad.
Señales de alarma:
⚠️ Todos los tests pasan al primer intento (sospechoso — tests reales
suelen fallar al menos una vez durante desarrollo)
⚠️ Los assertions usan valores que parecen "calculados" en vez de
"esperados por el negocio"
⚠️ No hay tests para edge cases o errores
⚠️ Los tests solo verifican status code, no el body del response
⚠️ Los tests no tienen descripción de qué escenario verifican
Ejemplo real:
def test_calculate_discount():
result = calculate_discount(Decimal("100"), "SAVE10")
assert result == Decimal("90.00") # ¿Cómo saben que 90 es correcto?
# Si el descuento debería ser $10 (no 10%),
# entonces 90 es correcto.
# Si debería ser 10% del subtotal y el subtotal es 100,
# entonces 90 es correcto.
# Pero si el descuento "SAVE10" es $10 off
# y el subtotal ya incluye tax, ¿es 90 correcto?
def test_calculate_tax():
result = calculate_tax(Decimal("100"), "CA")
assert result == Decimal("7.25")
# ¿7.25% es correcto para California? ¿En qué año?
# California state tax es 7.25%, pero counties agregan más.
# ¿El test verifica el rate correcto o el rate que AI hardcodeó?
Regla de detección: Para cada assertion, pregúntate: "¿puedo derivar este valor esperado de los requisitos de negocio, sin mirar el código?" Si la respuesta es no, el test podría estar confirmando un bug en vez de verificar funcionalidad correcta.
Red Flag 8: Manejo de Errores Cosmético
Qué es: AI genera try/except blocks que se ven profesionales pero que en realidad ocultan errores, catchean demasiado amplio, o manejan los errores equivocados mientras ignoran los reales.
Por qué AI lo hace: AI sabe que el error handling es "buena práctica", así que lo agrega. Pero no siempre sabe QUÉ errores son probables ni CÓMO manejarlos correctamente. El resultado es error handling que se ve bien pero no funciona.
Señales de alarma:
⚠️ except Exception: catch-all que oculta bugs
⚠️ try/except con pass (silencia errores)
⚠️ Error handling para errores imposibles pero no para probables
⚠️ Retry logic sin backoff o sin límite
⚠️ Logging del error pero no manejo real
⚠️ HTTPException(500) para todo tipo de error
Ejemplo real:
@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")
El problema:
⚠️ Si save_to_database falla con ConnectionError,
el procesamiento ya se hizo pero no se guardó.
El usuario recibe 503 y reintenta → procesamiento duplicado.
⚠️ Si notify_webhook falla,
el resultado ya se guardó en DB.
El usuario recibe error pero los datos sí se guardaron.
⚠️ except Exception oculta TODOS los demás errores.
¿TypeError? "Internal error."
¿ImportError? "Internal error."
No sabrás qué pasó.
Regla de detección: Para cada except block, pregúntate: "¿qué estado queda el sistema si este error ocurre?" Si no puedes responder, el error handling es cosmético.
Tabla de Resumen: Los 8 Red Flags
┌─────┬──────────────────────────────────────┬──────────┬───────────────┐
│ # │ Red Flag │ Riesgo │ Detección │
├─────┼──────────────────────────────────────┼──────────┼───────────────┤
│ 1 │ Sobre-ingeniería │ Medio │ Contar clases │
│ 2 │ APIs de versiones anteriores │ Alto │ Verificar docs│
│ 3 │ Abstracciones no pedidas │ Medio │ Contar archivos│
│ 4 │ Problema ligeramente diferente │ Alto │ Comparar prompt│
│ 5 │ Confianza sin correctitud │ Crítico │ Verificar lógica│
│ 6 │ Mezcla de frameworks │ Alto │ Revisar imports│
│ 7 │ Tests tautológicos │ Alto │ Derivar valores│
│ 8 │ Error handling cosmético │ Alto │ Trazar estados │
└─────┴──────────────────────────────────────┴──────────┴───────────────┘
Conexión con Proyecto
Red flags en el proyecto integrador (Módulo 8)
El codebase del proyecto integrador contiene al menos 3-4 de estos red flags plantados intencionalmente. No todos los problemas del proyecto son "bugs" — algunos son red flags que no causan errores inmediatos pero indican problemas de calidad o mantenibilidad.
Tu capacidad de detectar estos red flags es lo que diferencia un code review superficial ("compila, los tests pasan, apruebo") de un code review profesional ("compila y los tests pasan, pero hay 3 red flags que necesitan atención").
Troubleshooting
Problema 1: "No estoy seguro de si es sobre-ingeniería o buen diseño"
Causa: La línea entre "preparado para el futuro" y "sobre-ingeniería" es difusa. Solución: Pregúntate: "¿Necesito esta abstracción HOY para resolver el problema actual?" Si la respuesta es no, es sobre-ingeniería. YAGNI (You Ain't Gonna Need It). Si mañana necesitas la abstracción, la agregas mañana en 15 minutos. No la construyes hoy para un futuro que probablemente no llega.
Problema 2: "No conozco todas las versiones de las librerías"
Causa: Nadie las conoce todas. No necesitas memorizarlas. Solución: La regla es simple: si un import o función te genera duda, verifica. Abre la documentación oficial de la librería en la versión que usas. 2 minutos de verificación te ahorran 2 horas de debugging en producción. Con el tiempo, internalizarás los cambios más comunes (Pydantic v1 → v2, pyjwt < 2.0 → >= 2.0, etc.).
Problema 3: "No puedo distinguir si el código resuelve mi problema o uno diferente"
Causa: Tu prompt/requisito no es lo suficientemente específico. Solución: Antes del review, escribe en 2-3 oraciones qué debería hacer el código. Luego compara cada función con esas oraciones. Si hay funciones que no mapean a ninguna oración, son sospechosas. Si hay oraciones que no mapean a ninguna función, algo falta.
Problema 4: "Los tests pasan — ¿realmente necesito sospechar?"
Causa: Tests que pasan generan falsa confianza.
Solución: No preguntes "¿pasan los tests?" Pregunta "¿qué verifican los tests?" Un test que verifica assert response.status_code == 200 no te dice nada sobre si los datos son correctos. Lee las assertions, no los resultados.
Problema 5: "Mi equipo no entiende por qué marco red flags de AI — el código funciona"
Causa: Los red flags de AI no son bugs inmediatos — son indicadores de problemas futuros. Solución: Explícalo así: "El código funciona hoy. Pero usa una API deprecated que dejará de funcionar en la próxima versión. Mejor arreglarlo ahora en 5 minutos que debuggear en producción cuando se rompa." Los red flags son mantenimiento preventivo, no reparación de emergencia.
Ejercicios
Ejercicio 1: Identificar el red flag (Fácil)
Para cada snippet, identifica cuál de los 8 red flags está presente:
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)
# Solo hay una implementación. No hay SMS, Push, ni Slack.
Snippet C:
def test_user_age():
user = create_user(birth_year=1990)
assert user.age == 34 # Hardcoded — ¿y en 2026?
Ver solución
Snippet A → Red Flag 6: Mezcla de frameworks. Django models en un proyecto FastAPI. Django ORM y FastAPI no son compatibles directamente.
Snippet B → Red Flag 3: Abstracciones no pedidas (y parcialmente Red Flag 1: sobre-ingeniería). Una clase abstracta con una sola implementación es una abstracción prematura. Si mañana necesitas Slack notifications, la agregas en 5 minutos. Hoy, la abstracción agrega complejidad sin valor.
Snippet C → Red Flag 7: Tests tautológicos. El test hardcodea 34 como edad esperada. En 2025 es correcto, en 2026 falla. El test verifica un valor calculado por el developer al momento de escribir el test, no un valor derivado de la lógica de negocio. Debería ser: assert user.age == datetime.now().year - 1990.
Ejercicio 2: Red flag hunting (Medio)
Este código fue generado por Claude Code. Encuentra todos los red flags (hay al menos 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
Ver solución
Red Flag 1: Sobre-ingeniería (BaseRepository). Un ABC con 3 métodos abstractos para un in-memory dict. La implementación TaskRepository no agrega valor sobre el dict directo. Para un CRUD con un solo tipo de entidad, esto es innecesario.
Red Flag 2: API de versión anterior (Pydantic v1). @validator (v1) en vez de @field_validator (v2). class Config: orm_mode = True (v1) en vez de model_config = ConfigDict(from_attributes=True) (v2). Si el proyecto usa Pydantic v2, este código no funciona correctamente.
Red Flag 3: Abstracciones no pedidas. Para un "crea un endpoint de tareas", AI generó un Repository pattern con interface abstracta. Solo se necesitaba un dict y dos funciones.
Red Flag 8: Error handling cosmético. except Exception en el POST catchea TODO — incluyendo TypeError, KeyError, o cualquier bug en la lógica. Todos se convierten en "Error creating task" 500. Un bug real se oculta detrás de un mensaje genérico.
Bonus — No es red flag de AI pero sí bug:
id: str(len(repo.tasks) + 1)— IDs se reutilizan si borras tareas.TaskPriority(int)no es un Enum — es una clase que hereda de int.TaskPriority.LOWno existe como se espera.datetime.utcnow()— deprecated en Python 3.12+.
Ejercicio 3: Rewrite sin red flags (Medio)
Toma el código del Ejercicio 2 y reescríbelo eliminando todos los red flags. Mantén la misma funcionalidad.
Ver solución
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
Cambios:
- ❌ Eliminado BaseRepository y TaskRepository → dict directo
- ❌ Eliminada clase TaskPriority inútil
- ✅ Pydantic v2:
@field_validator+@classmethod - ✅ UUID en vez de len()+1 para IDs
- ✅
datetime.now(timezone.utc)en vez deutcnow() - ✅ Sin
except Exceptioncatch-all - ✅
response_modelystatus_code=201 - ✅ 40 líneas en vez de 70+ — misma funcionalidad
Ejercicio 4: Crear tu catálogo de red flags (Difícil)
Genera 3 piezas de código con Claude Code (o tu AI tool) y documenta los red flags que encuentres. Para cada uno:
- El prompt que usaste
- El código generado (relevante)
- Los red flags identificados (cuál de los 8)
- La severidad
- La corrección
Ver guía de evaluación
Tu catálogo debería:
- ✅ Tener al menos 3 generaciones diferentes (no el mismo prompt 3 veces)
- ✅ Cada generación con al menos 1 red flag identificado
- ✅ Red flags clasificados por número (1-8)
- ✅ Severidad justificada
- ✅ Corrección concreta (no "arreglarlo")
Si no encuentras red flags en 3 generaciones, tu prompt es demasiado simple. Intenta con: servicios que integran APIs externas, lógica de negocio con reglas específicas, o middleware personalizado.
Resumen
En esta cápsula aprendiste:
- Los 8 red flags específicos de código AI que no existen en código humano
- Red Flag 1 (Sobre-ingeniería): patterns para problemas que no los necesitan
- Red Flag 2 (APIs anteriores): funciones deprecated o de versiones viejas
- Red Flag 3 (Abstracciones fantasma): capas de indirección que nadie pidió
- Red Flag 4 (Problema diferente): código que resuelve algo similar pero no idéntico
- Red Flag 5 (Confianza sin correctitud): código profesional con lógica incorrecta
- Red Flag 6 (Mezcla frameworks): patrones de un framework en otro
- Red Flag 7 (Tests tautológicos): tests que confirman el código, no los requisitos
- Red Flag 8 (Error handling cosmético): try/except que se ve bien pero oculta bugs
- Reglas de detección rápida para cada red flag
- Estos red flags son conocimiento especializado que te diferencia de developers que solo revisan código humano
Próxima cápsula: Verificar Lógica de Negocio — la parte más difícil y más importante del code review de AI.
Recursos Adicionales
- Martin Fowler — Refactoring: Improving the Design of Existing Code - Referencia para distinguir entre buen diseño y sobre-ingeniería
- YAGNI — You Aren't Gonna Need It - Principio que aplica directamente a Red Flags 1 y 3
- Pydantic — Migration Guide v1 to v2 - Referencia para detectar Red Flag 2 en Pydantic
- PyJWT — Changelog - Cambios de API que AI no siempre refleja
- FastAPI vs Flask — Key Differences - Referencia para detectar Red Flag 6
- Google — Writing Clean Tests - Principios para detectar Red Flag 7
Debugging & Code Review with Claude Code — Módulo 4, Cápsula 04 Claude Code Agentic Development Path — Guía #6 de 11