Un code smell, u olor de código, es una señal de que el código puede tener un problema de diseño, legibilidad o mantenibilidad. No siempre es un error directo, pero indica que conviene revisar con atención.
En este tema aprenderemos a diagnosticar code smells de forma práctica: observar síntomas, formular hipótesis, evaluar riesgo y decidir si conviene intervenir ahora o dejar una nota para más adelante.
Un código puede funcionar y aun así oler mal. La diferencia es importante: un bug demuestra que el comportamiento es incorrecto; un code smell sugiere que el código puede ser difícil de entender, probar o modificar.
Observa este ejemplo:
def calcular(a, b, c, d):
if d:
return a * b - c
return a * b
La función puede devolver resultados correctos, pero los nombres no explican la intención y el parámetro d actúa como una bandera poco clara. Eso no prueba un bug, pero sí señala riesgo de mantenimiento.
Al revisar código Python, algunas señales aparecen con frecuencia:
Un diagnóstico útil debe ser concreto. No alcanza con decir “este código está feo”. Conviene describir qué se observa, qué riesgo produce y qué mejora podría reducir ese riesgo.
Ejemplo de diagnóstico pobre:
Ejemplo de diagnóstico útil:
Podemos usar una estructura breve para registrar smells:
Aplicado al proyecto ventas_demo:
Los nombres pobres obligan a interpretar el código línea por línea. Este smell aparece cuando una variable o función no comunica su intención.
def p(x):
r = []
for i in x:
if i["a"]:
r.append(i["e"])
return r
Una versión más clara puede ser:
def obtener_emails_activos(usuarios):
emails = []
for usuario in usuarios:
if usuario["activo"]:
emails.append(usuario["email"])
return emails
El comportamiento puede ser el mismo, pero la segunda versión reduce el esfuerzo de lectura.
Una función larga no es mala solo por tener muchas líneas. El problema aparece cuando contiene varias responsabilidades o varios niveles de decisión.
def procesar_pedido(pedido):
total = 0
for producto in pedido["productos"]:
total += producto["precio"] * producto["cantidad"]
if pedido["cliente"] == "vip":
total -= total * 0.15
if pedido["pais"] == "AR":
total += total * 0.21
if total < 10000:
total += 1500
print(f"Total: {total}")
return round(total, 2)
Esta función calcula, aplica reglas, imprime y redondea. El smell está en la mezcla de responsabilidades.
Un valor mágico es un número o texto importante que aparece sin explicar su significado.
if total < 10000:
total += 1500
Una versión más clara usa constantes:
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
if total < LIMITE_ENVIO_GRATIS:
total += COSTO_ENVIO
Además de mejorar lectura, las constantes reducen la duplicación cuando la misma regla aparece en varios lugares.
Los condicionales complejos son difíciles de revisar y fáciles de romper al modificarlos.
if usuario["activo"] and usuario["edad"] >= 18 and usuario["email"] != "" and not usuario["bloqueado"]:
enviar_promocion(usuario)
Podemos nombrar la condición:
es_usuario_habilitado = (
usuario["activo"]
and usuario["edad"] >= 18
and usuario["email"] != ""
and not usuario["bloqueado"]
)
if es_usuario_habilitado:
enviar_promocion(usuario)
El próximo paso podría ser extraer una función con un nombre de dominio, pero en este tema nos concentramos en reconocer la señal.
La duplicación es peligrosa porque una regla puede cambiarse en un lugar y olvidarse en otro.
def calcular_total_minorista(total):
if total < 10000:
total += 1500
return total
def calcular_total_mayorista(total):
if total < 10000:
total += 1500
return total
Si cambia el costo de envío, hay que recordar modificar ambas funciones. Esa repetición es una señal clara de riesgo.
Cuando una función mezcla reglas de negocio con lectura de archivos, red, fecha actual o consola, se vuelve más difícil de probar y modificar.
from datetime import date
def generar_factura(productos):
total = sum(producto["precio"] for producto in productos)
nombre_archivo = f"factura-{date.today().isoformat()}.txt"
with open(nombre_archivo, "w", encoding="utf-8") as archivo:
archivo.write(str(total))
return total
El cálculo del total está mezclado con fecha actual y escritura en disco. Si solo queremos probar el cálculo, la función nos obliga a manejar archivos.
Capturar una excepción y no hacer nada puede ocultar fallas reales.
def cargar_precio(datos):
try:
return float(datos["precio"])
except Exception:
return 0
El problema no es usar try. El problema es capturar cualquier error sin distinguir si faltó la clave, si el valor tenía formato inválido o si ocurrió otro problema inesperado.
Revisa la función calcular_total_venta del proyecto ventas_demo. Aunque ya mejoramos nombres y estilo, todavía puede contener smells de diseño.
Preguntas útiles:
No corrijas todo todavía. El objetivo es diagnosticar con precisión.
No todos los smells tienen la misma urgencia. En un proyecto real, conviene priorizar según riesgo, frecuencia de cambio y cercanía con una tarea actual.
Ruff, Black e isort ayudan a detectar o corregir problemas concretos, pero muchos code smells requieren criterio humano. Una herramienta puede señalar una variable no usada, pero no siempre puede decidir si una función tiene demasiadas responsabilidades de negocio.
Usa herramientas para reducir ruido y usa revisión humana para entender intención, dominio y riesgos de cambio.
Analiza el siguiente código y registra al menos cuatro smells usando la plantilla señal, riesgo, evidencia y acción posible.
def hacer(datos):
res = []
for d in datos:
try:
if d["a"] == 1 and d["m"] != "":
valor = d["p"] * d["c"]
if valor > 5000:
valor = valor - 300
res.append({"m": d["m"], "v": valor})
except Exception:
pass
return res
Algunas señales posibles son nombres poco claros, valores mágicos, condicional complejo, captura silenciosa de errores y mezcla de validación con cálculo.
Una versión más clara podría ser:
ESTADO_ACTIVO = 1
LIMITE_DESCUENTO = 5000
DESCUENTO = 300
def calcular_importe(precio, cantidad):
importe = precio * cantidad
if importe > LIMITE_DESCUENTO:
return importe - DESCUENTO
return importe
def obtener_resultados_validos(datos):
resultados = []
for item in datos:
if item["activo"] != ESTADO_ACTIVO:
continue
if item["email"] == "":
continue
importe = calcular_importe(item["precio"], item["cantidad"])
resultados.append({"email": item["email"], "importe": importe})
return resultados
Esta mejora no pretende ser definitiva. Su objetivo es mostrar cómo un diagnóstico concreto orienta cambios concretos.
Elige una función de ventas_demo y realiza estas tareas:
Usa este comando para verificar comportamiento:
python -m pytest
Antes de continuar, verifica que puedes hacer lo siguiente:
En este tema estudiamos los code smells como señales de riesgo. Vimos que no siempre indican errores inmediatos, pero ayudan a anticipar problemas de mantenimiento, legibilidad y diseño.
En el próximo tema profundizaremos en uno de los smells más comunes: funciones largas y funciones que hacen demasiadas cosas.