Módulo 5: Patrones de Error Comunes
Naming y Abstracciones Incorrectas
Naming y Abstracciones Incorrectas
Descripción de la cápsula
De todos los errores que AI genera, naming incorrecto y abstracciones mal aplicadas son los más dañinos a largo plazo. Un SQL injection es grave, pero es un bug puntual que corriges una vez. Una función llamada get_user que en realidad también modifica estado, valida permisos y envía un email — eso es una bomba de tiempo que causa bugs acumulativos durante meses.
AI es particularmente propensa a este tipo de error porque optimiza por patrones estadísticos. Ha visto millones de funciones llamadas process_data, handle_request, y get_user. Replica esos nombres genéricos sin considerar si son descriptivos para tu caso. Aplica Factory pattern porque lo vio en mucho código Java, aunque tu aplicación Python solo necesita un if/else.
En esta cápsula vas a entrenar tu ojo para detectar estos patrones. Cada ejemplo sigue la estructura: (a) el código que AI genera, (b) por qué se ve bien a primera vista, (c) el problema real, (d) la corrección correcta.
Patrón 1: Funciones con Nombres Engañosos
El problema
AI genera funciones cuyos nombres prometen una cosa pero hacen varias. El nombre describe solo una fracción de lo que la función realmente hace.
Código que AI genera
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
app = FastAPI()
class User(BaseModel):
id: int
username: str
email: str
is_active: bool = True
last_login: datetime | None = None
users_db: dict[int, dict] = {}
async def get_user(user_id: int) -> User:
"""Obtiene un usuario por ID."""
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
user_data = users_db[user_id]
user_data["last_login"] = datetime.now()
users_db[user_id] = user_data
if not user_data["is_active"]:
send_reactivation_email(user_data["email"])
user_data["is_active"] = True
users_db[user_id] = user_data
return User(**user_data)
def send_reactivation_email(email: str) -> None:
msg = MIMEText("Tu cuenta ha sido reactivada.")
msg["Subject"] = "Cuenta reactivada"
msg["From"] = "noreply@app.com"
msg["To"] = email
with smtplib.SMTP("localhost") as server:
server.send_message(msg)
Por qué se ve bien a primera vista
- La función tiene un docstring claro: "Obtiene un usuario por ID"
- El nombre
get_useres intuitivo y sigue convenciones - El código compila, los types están correctos
- La estructura del código es limpia
El problema real
La función get_user hace tres cosas:
- Obtiene el usuario (lo que el nombre promete)
- Modifica
last_login(efecto secundario oculto) - Reactiva usuarios inactivos y envía email (efecto secundario con side effects externos)
¿Por qué esto es una bomba de tiempo?
Escenarios que van a causar bugs:
1. Un developer usa get_user() en un endpoint de solo lectura
→ Sin saberlo, modifica last_login en cada consulta
→ Los analytics de "último login real" quedan corruptos
2. Un test unitario llama get_user() para verificar datos
→ El test modifica estado como efecto secundario
→ Los tests siguientes fallan de forma intermitente
3. Un dashboard admin consulta usuarios frecuentemente
→ Cada consulta envía email de reactivación a inactivos
→ Los usuarios reciben 50 emails diarios
4. Otro developer lee el nombre y asume que es idempotente
→ Cachea el resultado de get_user()
→ La reactivación nunca se ejecuta para usuarios reales
La corrección
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class User(BaseModel):
id: int
username: str
email: str
is_active: bool = True
last_login: datetime | None = None
users_db: dict[int, dict] = {}
async def get_user_by_id(user_id: int) -> User:
"""Obtiene un usuario por ID. Operación de solo lectura."""
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return User(**users_db[user_id])
async def record_user_login(user_id: int) -> None:
"""Registra el timestamp del login actual."""
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
users_db[user_id]["last_login"] = datetime.now()
async def send_reactivation_notification(email: str) -> None:
"""Envía notificación de reactivación al usuario."""
msg = MIMEText("Tu cuenta ha sido reactivada.")
msg["Subject"] = "Cuenta reactivada"
msg["From"] = "noreply@app.com"
msg["To"] = email
with smtplib.SMTP("localhost") as server:
server.send_message(msg)
async def reactivate_user(user_id: int) -> User:
"""Reactiva un usuario inactivo y envía notificación."""
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
user_data = users_db[user_id]
if not user_data["is_active"]:
user_data["is_active"] = True
users_db[user_id] = user_data
await send_reactivation_notification(user_data["email"])
return User(**user_data)
Por qué la corrección es mejor:
- Cada función hace exactamente lo que su nombre dice
get_user_by_ides de solo lectura — segura para cachear, testear, y usar en cualquier contextorecord_user_logines explícita sobre la mutaciónreactivate_useres explícita sobre el efecto secundario (email)- El developer que lee los nombres sabe exactamente qué esperar
Señal de alerta
Cuando veas una función en AI code con un nombre tipo get_X, fetch_X, o load_X, verifica que solo haga lectura. Si modifica estado, el nombre engaña.
Patrón 2: Nombres Genéricos que Ocultan Intención
El problema
AI genera funciones con nombres tan genéricos que no transmiten ningún significado específico. process_data, handle_request, do_operation — estos nombres podrían aplicarse a cualquier función.
Código que AI genera
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class OrderItem(BaseModel):
product_id: int
quantity: int
unit_price: float
class Order(BaseModel):
id: int
items: list[OrderItem]
created_at: datetime
status: str = "pending"
orders_db: dict[int, dict] = {}
def process_data(data: dict) -> dict:
"""Procesa los datos recibidos."""
total = 0
processed_items = []
for item in data.get("items", []):
subtotal = item["quantity"] * item["unit_price"]
if item["quantity"] > 10:
subtotal *= 0.9
processed_items.append({
**item,
"subtotal": round(subtotal, 2)
})
total += subtotal
if total > 500:
total *= 0.95
data["items"] = processed_items
data["total"] = round(total, 2)
data["processed"] = True
return data
def handle_request(request_data: dict) -> dict:
"""Maneja el request."""
validated = validate_input(request_data)
result = process_data(validated)
return format_output(result)
def validate_input(data: dict) -> dict:
"""Valida el input."""
if "items" not in data:
raise ValueError("Items required")
return data
def format_output(data: dict) -> dict:
"""Formatea el output."""
return {
"order": data,
"status": "success",
"timestamp": datetime.now().isoformat()
}
Por qué se ve bien a primera vista
- Las funciones son cortas y siguen el patrón validate → process → format
- Hay separación de concerns aparente
- El código tiene docstrings
- La estructura es limpia
El problema real
Lee solo los nombres de las funciones:
process_data() → ¿Qué datos? ¿Qué tipo de procesamiento?
handle_request() → ¿Qué request? ¿De qué endpoint?
validate_input() → ¿Qué valida específicamente?
format_output() → ¿Qué formato? ¿Para quién?
Sin leer la implementación, no sabes nada. Compara con nombres descriptivos:
calculate_order_totals_with_discounts() → Sabes exactamente qué hace
process_new_order_submission() → Sabes el contexto
validate_order_has_items() → Sabes qué valida
format_order_api_response() → Sabes el formato y destino
Además, process_data usa dict como tipo en vez de modelos tipados — otro patrón frecuente de AI donde se pierde type safety.
La corrección
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class OrderItem(BaseModel):
product_id: int
quantity: int
unit_price: float
subtotal: float = 0.0
class OrderRequest(BaseModel):
items: list[OrderItem]
class OrderSummary(BaseModel):
items: list[OrderItem]
subtotal_before_discount: float
discount_applied: float
total: float
BULK_DISCOUNT_THRESHOLD = 10
BULK_DISCOUNT_RATE = 0.10
ORDER_DISCOUNT_THRESHOLD = 500.0
ORDER_DISCOUNT_RATE = 0.05
def calculate_order_totals_with_discounts(order: OrderRequest) -> OrderSummary:
"""Calcula subtotales por item (descuento por volumen >10 unidades)
y total del pedido (descuento 5% si supera $500)."""
items_with_subtotals = []
subtotal_before_discount = 0.0
for item in order.items:
item_subtotal = item.quantity * item.unit_price
if item.quantity > BULK_DISCOUNT_THRESHOLD:
item_subtotal *= (1 - BULK_DISCOUNT_RATE)
items_with_subtotals.append(item.model_copy(update={"subtotal": round(item_subtotal, 2)}))
subtotal_before_discount += item_subtotal
discount_applied = 0.0
total = subtotal_before_discount
if total > ORDER_DISCOUNT_THRESHOLD:
discount_applied = total * ORDER_DISCOUNT_RATE
total -= discount_applied
return OrderSummary(
items=items_with_subtotals,
subtotal_before_discount=round(subtotal_before_discount, 2),
discount_applied=round(discount_applied, 2),
total=round(total, 2),
)
Por qué la corrección es mejor:
- El nombre de la función dice exactamente qué calcula
- Los modelos Pydantic reemplazan dicts genéricos
- Las constantes de negocio (umbrales, tasas) son explícitas, no magic numbers
- Un developer nuevo entiende el propósito leyendo solo el nombre y la firma
Señal de alerta
Nombres que empiezan con process_, handle_, manage_, do_, u operation_ son casi siempre demasiado genéricos. Si no puedes saber qué hace la función leyendo solo su nombre, el nombre es incorrecto.
Patrón 3: Abstracciones Prematuras — Factory Innecesario
El problema
AI aplica patrones de diseño por frecuencia estadística, no por necesidad. Ha visto Factory pattern en miles de repositorios y lo aplica incluso cuando un simple if/else resolvería el problema en 5 líneas.
Código que AI genera
from abc import ABC, abstractmethod
from pydantic import BaseModel
class NotificationBase(BaseModel):
recipient: str
message: str
class Notification(ABC):
@abstractmethod
def send(self, notification: NotificationBase) -> bool:
pass
@abstractmethod
def validate(self, notification: NotificationBase) -> bool:
pass
class EmailNotification(Notification):
def send(self, notification: NotificationBase) -> bool:
print(f"Sending email to {notification.recipient}: {notification.message}")
return True
def validate(self, notification: NotificationBase) -> bool:
return "@" in notification.recipient
class SMSNotification(Notification):
def send(self, notification: NotificationBase) -> bool:
print(f"Sending SMS to {notification.recipient}: {notification.message}")
return True
def validate(self, notification: NotificationBase) -> bool:
return notification.recipient.startswith("+")
class PushNotification(Notification):
def send(self, notification: NotificationBase) -> bool:
print(f"Sending push to {notification.recipient}: {notification.message}")
return True
def validate(self, notification: NotificationBase) -> bool:
return len(notification.recipient) > 0
class NotificationFactory:
_registry: dict[str, type[Notification]] = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
@classmethod
def create(cls, notification_type: str) -> Notification:
if notification_type not in cls._registry:
raise ValueError(f"Unknown notification type: {notification_type}")
return cls._registry[notification_type]()
@classmethod
def register(cls, notification_type: str, notification_class: type[Notification]) -> None:
cls._registry[notification_type] = notification_class
def send_notification(notification_type: str, recipient: str, message: str) -> bool:
factory = NotificationFactory()
notifier = factory.create(notification_type)
notification = NotificationBase(recipient=recipient, message=message)
if not notifier.validate(notification):
raise ValueError(f"Invalid recipient for {notification_type}")
return notifier.send(notification)
Por qué se ve bien a primera vista
- Sigue un patrón de diseño reconocido (Factory)
- Tiene clase abstracta, implementaciones concretas, y factory
- Es "extensible" — puedes agregar nuevos tipos de notificación
- El código está bien estructurado con separación de clases
El problema real
Pregúntate: ¿cuántas veces vas a agregar un nuevo tipo de notificación? En la mayoría de aplicaciones, los tipos de notificación se definen una vez y rara vez cambian. Esta abstracción:
- Agrega 70 líneas de código para hacer lo que 20 líneas resolverían
- Crea 6 clases/archivos donde 1 función basta
- Introduce indirección que dificulta debugging
- La "extensibilidad" nunca se usa — YAGNI (You Ain't Gonna Need It)
- El registro dinámico
register()es una puerta abierta a bugs de runtime
Costo de la abstracción prematura:
Lectura: Tienes que navegar 6 clases para entender qué hace send_notification
Debugging: Si email falla, ¿el bug está en Factory, en EmailNotification, o en Notification?
Testing: Necesitas tests para cada clase + la factory + la integración
Onboarding: Un developer nuevo tarda 3x más en entender este código
Cambios: Agregar un campo a NotificationBase requiere tocar todas las subclases
La corrección
from pydantic import BaseModel, field_validator
from enum import Enum
class NotificationType(str, Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
class NotificationRequest(BaseModel):
notification_type: NotificationType
recipient: str
message: str
@field_validator("recipient")
@classmethod
def validate_recipient(cls, v: str, info) -> str:
notification_type = info.data.get("notification_type")
if notification_type == NotificationType.EMAIL and "@" not in v:
raise ValueError("Email recipient must contain @")
if notification_type == NotificationType.SMS and not v.startswith("+"):
raise ValueError("SMS recipient must start with +")
if not v:
raise ValueError("Recipient cannot be empty")
return v
def send_notification(request: NotificationRequest) -> bool:
"""Envía una notificación por el canal especificado."""
if request.notification_type == NotificationType.EMAIL:
return _send_email(request.recipient, request.message)
elif request.notification_type == NotificationType.SMS:
return _send_sms(request.recipient, request.message)
elif request.notification_type == NotificationType.PUSH:
return _send_push(request.recipient, request.message)
raise ValueError(f"Unsupported type: {request.notification_type}")
def _send_email(recipient: str, message: str) -> bool:
print(f"Sending email to {recipient}: {message}")
return True
def _send_sms(recipient: str, message: str) -> bool:
print(f"Sending SMS to {recipient}: {message}")
return True
def _send_push(recipient: str, message: str) -> bool:
print(f"Sending push to {recipient}: {message}")
return True
Por qué la corrección es mejor:
- ~40 líneas vs ~70 líneas — más fácil de leer y mantener
- Un solo punto de entrada (
send_notification) — fácil de debuggear - Pydantic maneja la validación — no necesitas clases abstractas
- El Enum garantiza type safety — no puedes pasar un tipo inválido
- Si en el futuro necesitas el patrón Factory, refactorizas entonces (no antes)
Cuándo SÍ usar Factory
Factory tiene sentido cuando:
- Tienes 10+ implementaciones que cambian frecuentemente
- Las implementaciones se cargan desde plugins o configuración externa
- Diferentes equipos agregan implementaciones independientemente
- La creación del objeto requiere lógica compleja variable
Si ninguna de estas aplica, un if/else es la abstracción correcta.
Patrón 4: Herencia Profunda Innecesaria
El problema
AI genera jerarquías de herencia de 3-4 niveles donde composición o funciones simples bastarían. Esto viene de su entrenamiento con código Java/C# donde la herencia profunda era más común.
Código que AI genera
from datetime import datetime
from pydantic import BaseModel
class BaseEntity(BaseModel):
id: int | None = None
created_at: datetime = datetime.now()
updated_at: datetime = datetime.now()
def save(self) -> None:
self.updated_at = datetime.now()
class BaseUserEntity(BaseEntity):
username: str
email: str
def get_display_name(self) -> str:
return self.username
class BaseActiveUserEntity(BaseUserEntity):
is_active: bool = True
last_login: datetime | None = None
def deactivate(self) -> None:
self.is_active = False
def record_login(self) -> None:
self.last_login = datetime.now()
class AdminUser(BaseActiveUserEntity):
permissions: list[str] = []
admin_level: int = 1
def has_permission(self, permission: str) -> bool:
return permission in self.permissions
def grant_permission(self, permission: str) -> None:
if permission not in self.permissions:
self.permissions.append(permission)
class SuperAdminUser(AdminUser):
can_delete_users: bool = True
can_modify_system: bool = True
def has_permission(self, permission: str) -> bool:
return True
Por qué se ve bien a primera vista
- Cada nivel de la jerarquía agrega funcionalidad
- Los nombres siguen una progresión lógica
- SuperAdmin "es un" AdminUser, AdminUser "es un" ActiveUser...
- El código es DRY — no repite campos
El problema real
Jerarquía de herencia:
SuperAdminUser → AdminUser → BaseActiveUserEntity → BaseUserEntity → BaseEntity → BaseModel
5 niveles de herencia para un modelo de usuario.
Los problemas concretos:
-
Rigidez: ¿Qué si necesitas un usuario activo que NO es admin pero tiene
permissions? No encaja en la jerarquía. -
Fragilidad: Un cambio en
BaseEntity.save()afecta a todas las clases hijas. Un bug en nivel 2 se propaga a niveles 3, 4 y 5. -
Diamond problem: Si en el futuro necesitas
ModeratorUserque hereda deAdminUserpero con reglas deBaseActiveUserEntitydiferentes, la herencia se rompe. -
datetime.now()en default: Cada instancia comparte el mismo timestamp (evaluado al cargar la clase, no al crear la instancia). -
Testing: Para testear
SuperAdminUser, necesitas entender 5 niveles de herencia.
La corrección
from datetime import datetime
from pydantic import BaseModel, Field
from enum import Enum
class UserRole(str, Enum):
USER = "user"
ADMIN = "admin"
SUPER_ADMIN = "super_admin"
class User(BaseModel):
id: int | None = None
username: str
email: str
role: UserRole = UserRole.USER
is_active: bool = True
permissions: list[str] = Field(default_factory=list)
last_login: datetime | None = None
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
def has_permission(user: User, permission: str) -> bool:
"""SuperAdmin tiene todos los permisos. Otros verifican su lista."""
if user.role == UserRole.SUPER_ADMIN:
return True
return permission in user.permissions
def grant_permission(user: User, permission: str) -> User:
"""Retorna una copia del usuario con el permiso agregado."""
if permission not in user.permissions:
updated_permissions = [*user.permissions, permission]
return user.model_copy(update={"permissions": updated_permissions})
return user
def record_login(user: User) -> User:
"""Retorna una copia del usuario con last_login actualizado."""
return user.model_copy(update={"last_login": datetime.now()})
def deactivate_user(user: User) -> User:
"""Retorna una copia del usuario desactivado."""
return user.model_copy(update={"is_active": False})
Por qué la corrección es mejor:
- Un solo modelo
Usercon un camporole— sin jerarquía - Funciones puras que retornan copias — sin mutación oculta
Field(default_factory=datetime.now)resuelve el bug del timestamp compartido- Agregar un nuevo rol solo requiere extender el Enum
- Testing es trivial: creas un
Usercon los campos que necesitas
Señal de alerta
Cuando veas herencia de más de 2 niveles en código AI, pregúntate: "¿Podría resolver esto con composición (un campo que define el tipo) en vez de herencia?" La respuesta casi siempre es sí.
Patrón 5: Patrón de Diseño Mal Aplicado — Singleton Innecesario
El problema
AI aplica Singleton pattern a clases que no lo necesitan. Singleton tiene sentido para recursos verdaderamente globales (connection pools, configuración de sistema). AI lo usa para cualquier clase que "debería tener una sola instancia."
Código que AI genera
from threading import Lock
class DatabaseConfig:
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.host = "localhost"
self.port = 5432
self.database = "myapp"
self.username = "admin"
self.password = "secret123"
self._initialized = True
def get_connection_string(self) -> str:
return f"postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
class Logger:
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.logs: list[str] = []
self._initialized = True
def log(self, message: str) -> None:
self.logs.append(message)
def get_logs(self) -> list[str]:
return self.logs.copy()
config = DatabaseConfig()
logger = Logger()
Por qué se ve bien a primera vista
- Singleton es un patrón reconocido
- El double-checked locking es la implementación "correcta"
- La intención (una sola instancia) parece razonable
- El código es thread-safe
El problema real
-
DatabaseConfig como Singleton: La configuración debería cargarse desde variables de entorno, no hardcodeada. El Singleton hace imposible tener diferentes configuraciones para dev/staging/production sin modificar la clase.
-
Logger como Singleton: Python ya tiene
loggingmodule que maneja esto. Reinventar logging con una lista es perder funcionalidades (niveles, formateadores, handlers). -
Testing: Los Singletons hacen tests dependientes entre sí. Si un test modifica el Singleton, afecta todos los tests posteriores. Necesitas teardown manual.
-
Password hardcoded:
"secret123"directamente en el código. Un problema de seguridad grave que el patrón Singleton oculta visualmente. -
Acoplamiento global: Todo el código depende de la instancia global. Cambiar la configuración requiere modificar la clase.
La corrección
from pydantic_settings import BaseSettings
from functools import lru_cache
import logging
class DatabaseConfig(BaseSettings):
host: str = "localhost"
port: int = 5432
database: str = "myapp"
username: str = "admin"
password: str
model_config = {"env_prefix": "DB_"}
@property
def connection_string(self) -> str:
return f"postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
@lru_cache
def get_database_config() -> DatabaseConfig:
"""Singleton funcional: cachea la instancia pero permite override en tests."""
return DatabaseConfig()
def get_logger(name: str) -> logging.Logger:
"""Usa el sistema de logging estándar de Python."""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
Por qué la corrección es mejor:
BaseSettingscarga config desde variables de entorno automáticamentepasswordes obligatorio — no tiene default hardcoded@lru_cacheactúa como singleton funcional: cachea pero se puede limpiar conget_database_config.cache_clear()en tests- Usa
loggingestándar de Python en vez de reinventar la rueda - Testing:
get_database_config.cache_clear()limpia el cache entre tests
Señal de alerta
Cuando veas _instance = None y __new__ override en código AI, pregúntate: "¿Este recurso realmente necesita ser global y único?" En Python, @lru_cache en una función factory o un módulo-level variable suelen ser suficientes.
Resumen de Señales de Alerta
Checklist rápido de naming y abstracciones:
☐ Funciones get_/fetch_/load_ que modifican estado
→ Verificar que solo hagan lectura
☐ Nombres genéricos: process_, handle_, manage_, do_
→ Reemplazar con nombres que describan la acción específica
☐ Factory pattern con < 5 implementaciones
→ Considerar if/else o match/case
☐ Herencia de más de 2 niveles
→ Considerar composición con enums/campos
☐ Singleton con __new__ override
→ Considerar @lru_cache o módulo-level variable
☐ Clases abstractas con una sola implementación
→ Eliminar la abstracción, usar la implementación directa
☐ Magic numbers en lógica de negocio
→ Extraer a constantes con nombres descriptivos
Conexión con Proyecto
El proyecto integrador del módulo 8 contiene 3-4 errores de naming y abstracciones. Específicamente busca:
- ✅ Al menos una función con nombre engañoso que tiene side effects
- ✅ Al menos una abstracción prematura (pattern donde no se necesita)
- ✅ Nombres genéricos que dificultan entender qué hace el código
- ✅ Constantes de negocio como magic numbers
Lo que practicas en esta cápsula es exactamente lo que aplicarás en el proyecto.
Troubleshooting
"¿Cuándo SÍ es correcto usar nombres genéricos?"
En funciones de utilidad que realmente son genéricas. Un map(), filter(), o sort() son genéricos porque operan sobre cualquier dato. Pero una función que calcula descuentos de pedidos no es genérica — tiene un dominio específico.
"¿Cómo sé si una abstracción es prematura?"
La regla de tres: no crees una abstracción hasta que tengas al menos 3 implementaciones concretas. Si solo tienes 1-2, es prematura.
"¿AI siempre genera estos errores?"
No siempre, pero con alta frecuencia. Si tu prompt es específico sobre naming y estructura, AI genera mejor código. Prompts vagos → nombres vagos.
"¿Refactorizo todo el naming del proyecto o solo lo nuevo?"
Empieza con lo nuevo. Refactorizar naming existente es valioso pero introduce riesgo. Aplica la regla del Boy Scout: deja el código mejor de como lo encontraste, un nombre a la vez.
"¿Qué hago si el equipo usa nombres genéricos por convención?"
Respeta la convención del equipo pero sugiere mejoras gradualmente. Un process_order en un codebase donde todo se llama process_X es consistente, aunque no ideal.
Ejercicios
Ejercicio 1: Identificar naming engañoso
Analiza esta función. ¿Qué hace realmente vs lo que el nombre promete?
from datetime import datetime
users_db: dict[int, dict] = {
1: {"id": 1, "username": "alice", "email": "alice@example.com", "login_count": 5},
2: {"id": 2, "username": "bob", "email": "bob@example.com", "login_count": 0},
}
def check_user(user_id: int) -> bool:
"""Verifica si el usuario existe."""
if user_id not in users_db:
return False
user = users_db[user_id]
user["login_count"] += 1
user["last_checked"] = datetime.now().isoformat()
if user["login_count"] > 100:
user["status"] = "veteran"
return True
Ver solución
El nombre check_user promete verificación (lectura), pero hace 3 cosas:
- Verifica existencia (lo que promete)
- Incrementa
login_count(mutación oculta) - Asigna status "veteran" si
login_count > 100(lógica de negocio oculta)
Corrección:
from datetime import datetime
def user_exists(user_id: int) -> bool:
"""Verifica si el usuario existe. Solo lectura."""
return user_id in users_db
def increment_login_count(user_id: int) -> int:
"""Incrementa y retorna el nuevo login count."""
if user_id not in users_db:
raise KeyError(f"User {user_id} not found")
users_db[user_id]["login_count"] += 1
return users_db[user_id]["login_count"]
def update_veteran_status(user_id: int, threshold: int = 100) -> bool:
"""Marca al usuario como veteran si supera el threshold. Retorna True si se actualizó."""
if user_id not in users_db:
raise KeyError(f"User {user_id} not found")
user = users_db[user_id]
if user.get("login_count", 0) > threshold and user.get("status") != "veteran":
user["status"] = "veteran"
return True
return False
Cada función hace una sola cosa y su nombre lo refleja.
Ejercicio 2: Simplificar abstracción prematura
Este código usa Strategy pattern para calcular impuestos. Simplifica sin perder funcionalidad.
from abc import ABC, abstractmethod
class TaxStrategy(ABC):
@abstractmethod
def calculate(self, amount: float) -> float:
pass
class USATaxStrategy(TaxStrategy):
def calculate(self, amount: float) -> float:
return amount * 0.08
class EUTaxStrategy(TaxStrategy):
def calculate(self, amount: float) -> float:
return amount * 0.21
class TaxCalculator:
def __init__(self, strategy: TaxStrategy):
self.strategy = strategy
def get_tax(self, amount: float) -> float:
return self.strategy.calculate(amount)
calculator = TaxCalculator(USATaxStrategy())
tax = calculator.get_tax(100.0)
Ver solución
El Strategy pattern es excesivo para 2 implementaciones con una línea cada una.
from enum import Enum
class TaxRegion(str, Enum):
USA = "usa"
EU = "eu"
TAX_RATES: dict[TaxRegion, float] = {
TaxRegion.USA: 0.08,
TaxRegion.EU: 0.21,
}
def calculate_tax(amount: float, region: TaxRegion) -> float:
"""Calcula el impuesto según la región."""
rate = TAX_RATES.get(region)
if rate is None:
raise ValueError(f"No tax rate defined for region: {region}")
return amount * rate
tax = calculate_tax(100.0, TaxRegion.USA)
De 4 clases y ~25 líneas a 1 función y ~15 líneas. Si en el futuro necesitas lógica compleja por región (progresiva, exenciones), entonces refactorizas.
Ejercicio 3: Refactorizar herencia a composición
Convierte esta jerarquía de 3 niveles en un modelo plano con enum.
from pydantic import BaseModel
class BaseVehicle(BaseModel):
make: str
model: str
year: int
def describe(self) -> str:
return f"{self.year} {self.make} {self.model}"
class MotorVehicle(BaseVehicle):
engine_size: float
fuel_type: str = "gasoline"
def fuel_cost_per_km(self) -> float:
return self.engine_size * 0.05
class ElectricVehicle(BaseVehicle):
battery_capacity: float
range_km: int
def fuel_cost_per_km(self) -> float:
return self.battery_capacity * 0.001
Ver solución
from pydantic import BaseModel, model_validator
from enum import Enum
class PowertrainType(str, Enum):
GASOLINE = "gasoline"
DIESEL = "diesel"
ELECTRIC = "electric"
HYBRID = "hybrid"
COST_PER_KM = {
PowertrainType.GASOLINE: lambda engine_size: engine_size * 0.05,
PowertrainType.DIESEL: lambda engine_size: engine_size * 0.04,
PowertrainType.ELECTRIC: lambda battery_cap: battery_cap * 0.001,
PowertrainType.HYBRID: lambda engine_size: engine_size * 0.03,
}
class Vehicle(BaseModel):
make: str
model: str
year: int
powertrain: PowertrainType
engine_size: float | None = None
battery_capacity: float | None = None
range_km: int | None = None
@model_validator(mode="after")
def validate_powertrain_fields(self):
if self.powertrain == PowertrainType.ELECTRIC:
if self.battery_capacity is None:
raise ValueError("Electric vehicles require battery_capacity")
else:
if self.engine_size is None:
raise ValueError(f"{self.powertrain.value} vehicles require engine_size")
return self
def describe(self) -> str:
return f"{self.year} {self.make} {self.model} ({self.powertrain.value})"
def fuel_cost_per_km(self) -> float:
calculator = COST_PER_KM[self.powertrain]
if self.powertrain == PowertrainType.ELECTRIC:
return calculator(self.battery_capacity)
return calculator(self.engine_size)
Un solo modelo con validación condicional. Agregar HYBRID o HYDROGEN solo requiere una entrada en el Enum y en COST_PER_KM.
Ejercicio 4: Renombrar funciones genéricas
Estas funciones tienen nombres genéricos. Renómbralas basándote en lo que realmente hacen.
users_db: dict[int, dict] = {
1: {"id": 1, "username": "alice", "banned": False},
2: {"id": 2, "username": "bob", "banned": False},
}
def process(items: list[dict]) -> list[dict]:
return [item for item in items if item.get("status") == "active"]
def handle(data: dict) -> dict:
data["total"] = sum(item["price"] * item["qty"] for item in data["items"])
data["tax"] = data["total"] * 0.16
data["grand_total"] = data["total"] + data["tax"]
return data
def transform(records: list[dict]) -> dict[str, list[dict]]:
result: dict[str, list[dict]] = {}
for record in records:
category = record.get("category", "uncategorized")
result.setdefault(category, []).append(record)
return result
def execute(user_id: int, action: str) -> bool:
if action == "ban":
users_db[user_id]["banned"] = True
elif action == "unban":
users_db[user_id]["banned"] = False
return True
Ver solución
def filter_active_items(items: list[dict]) -> list[dict]:
"""Retorna solo los items con status 'active'."""
return [item for item in items if item.get("status") == "active"]
def calculate_order_totals_with_tax(order_data: dict, tax_rate: float = 0.16) -> dict:
"""Calcula subtotal, impuesto y gran total de un pedido."""
order_data["subtotal"] = sum(
item["price"] * item["qty"] for item in order_data["items"]
)
order_data["tax"] = order_data["subtotal"] * tax_rate
order_data["grand_total"] = order_data["subtotal"] + order_data["tax"]
return order_data
def group_records_by_category(records: list[dict]) -> dict[str, list[dict]]:
"""Agrupa registros por su campo 'category'."""
result: dict[str, list[dict]] = {}
for record in records:
category = record.get("category", "uncategorized")
result.setdefault(category, []).append(record)
return result
def set_user_ban_status(user_id: int, *, banned: bool) -> None:
"""Establece el estado de ban de un usuario."""
if user_id not in users_db:
raise KeyError(f"User {user_id} not found")
users_db[user_id]["banned"] = banned
Cada nombre describe la acción específica. Notas adicionales:
executeconaction: strse reemplaza porset_user_ban_statusconbanned: bool— elimina el string dispatchtax_ratese convierte en parámetro explícito, no magic numberset_user_ban_statusvalida que el usuario exista y usa keyword-only arg para claridad
Resumen
- Naming engañoso es el error más sutil: funciones
get_Xque modifican estado son bombas de tiempo - Nombres genéricos (
process_,handle_) no comunican intención — fuerzan a leer la implementación - Abstracciones prematuras (Factory, Strategy) agregan complejidad sin beneficio cuando hay pocas implementaciones
- Herencia profunda (3+ niveles) crea rigidez — composición con enums es más flexible en Python
- Singleton mal aplicado complica testing y oculta dependencias globales
- AI genera estos errores porque replica patrones estadísticos sin evaluar si aplican a tu contexto
- La regla general: el nombre de una función debe ser suficiente para entender qué hace sin leer su implementación
Recursos Adicionales
- Clean Code — Meaningful Names - Capítulo 2 de Clean Code sobre naming efectivo
- Refactoring Guru — Code Smells - Catálogo de code smells con refactorings sugeridos
- YAGNI — Martin Fowler - "You Aren't Gonna Need It" — por qué las abstracciones prematuras cuestan más de lo que ahorran
- Composition over Inheritance — Wikipedia - Principio fundamental de diseño orientado a objetos
- Python Design Patterns - Patrones de diseño idiomáticos en Python (no Java traducido)
- FastAPI Dependency Injection - Cómo FastAPI resuelve el problema de singletons con dependency injection
Siguiente cápsula: Edge Cases No Manejados — el patrón que causa crashes en producción.
Debugging & Code Review with Claude Code — Módulo 5, Cápsula 02 Claude Code Agentic Development Path — Guía #6 de 11