Este tema integra lo trabajado durante el curso. Partiremos de un pequeño proyecto Python con code smells intencionales, escribiremos pruebas de caracterización, ejecutaremos herramientas de calidad, diagnosticaremos problemas y aplicaremos mejoras pequeñas y seguras.
El objetivo no es dejar un proyecto perfecto, sino practicar un flujo profesional: entender, proteger, mejorar y verificar.
Desde una carpeta de trabajo, crea un proyecto nuevo:
mkdir calidad_integrador
cd calidad_integrador
python -m venv .venv
Activa el entorno virtual. En Windows PowerShell:
.venv\Scripts\Activate.ps1
En Linux o macOS:
source .venv/bin/activate
python -m pip install pytest black isort ruff mypy
Crea la estructura:
mkdir src
mkdir tests
New-Item src\pedidos.py
New-Item tests\test_pedidos.py
New-Item pyproject.toml
New-Item README.md
En Linux o macOS:
mkdir src tests
touch src/pedidos.py tests/test_pedidos.py pyproject.toml README.md
[project]
name = "calidad-integrador"
version = "0.1.0"
requires-python = ">=3.10"
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[tool.black]
line-length = 88
target-version = ["py310"]
[tool.isort]
profile = "black"
line_length = 88
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "B", "SIM", "I", "C901"]
[tool.ruff.lint.mccabe]
max-complexity = 6
[tool.mypy]
python_version = "3.10"
check_untyped_defs = true
warn_unused_configs = true
En src/pedidos.py, escribe:
import os
historial = []
def proc(items, cliente, pais, guardar, mostrar):
total = 0
for x in items:
try:
if x["cant"] > 0:
total = total + x["precio"] * x["cant"]
except Exception:
pass
if cliente == "vip":
total = total - total * 0.15
else:
if cliente == "regular":
total = total - total * 0.05
if pais == "AR":
total = total + total * 0.21
else:
if pais == "UY":
total = total + total * 0.22
else:
total = total + total * 0.19
if total < 10000:
total = total + 1500
total = round(total, 2)
historial.append(total)
if guardar:
with open("ultimo_pedido.txt", "w", encoding="utf-8") as archivo:
archivo.write(str(total))
if mostrar:
print("total", total)
return total
Antes de modificar, registra smells observados:
proc, items, x, cant.os.historial.guardar y mostrar.En tests/test_pedidos.py, agrega:
from pedidos import proc
def test_cliente_vip_argentina():
items = [
{"precio": 3000, "cant": 2},
{"precio": 1500, "cant": 1},
]
assert proc(items, "vip", "AR", False, False) == 9213.75
def test_cliente_regular_uruguay():
items = [
{"precio": 1000, "cant": 1},
]
assert proc(items, "regular", "UY", False, False) == 2659.0
def test_ignora_item_invalido_por_comportamiento_actual():
items = [
{"precio": 1000, "cant": 2},
{"precio": 5000},
]
assert proc(items, "nuevo", "CL", False, False) == 3880.0
Ejecuta:
python -m pytest
python -m ruff check src tests
python -m mypy src
python -m pytest
Es esperable que Ruff marque problemas como import no usado, excepción genérica o complejidad. No corrijas todo de golpe.
Primer cambio seguro: renombrar y extraer constantes. Mantén un alias temporal si las pruebas todavía importan proc.
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTOS = {"AR": 0.21, "UY": 0.22}
IMPUESTO_PREDETERMINADO = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
def calcular_total_pedido(productos, tipo_cliente, pais, guardar, mostrar):
total = 0
for producto in productos:
try:
if producto["cant"] > 0:
total += producto["precio"] * producto["cant"]
except Exception:
pass
...
proc = calcular_total_pedido
Ejecuta pruebas después del cambio.
Extrae funciones de reglas:
def calcular_subtotal(productos):
subtotal = 0
for producto in productos:
try:
if producto["cant"] > 0:
subtotal += producto["precio"] * producto["cant"]
except Exception:
pass
return subtotal
def obtener_descuento(tipo_cliente):
if tipo_cliente == "vip":
return DESCUENTO_VIP
if tipo_cliente == "regular":
return DESCUENTO_REGULAR
return 0
def obtener_impuesto(pais):
return IMPUESTOS.get(pais, IMPUESTO_PREDETERMINADO)
def aplicar_envio(total):
if total < LIMITE_ENVIO_GRATIS:
return total + COSTO_ENVIO
return total
def calcular_total_pedido(productos, tipo_cliente, pais, guardar, mostrar):
subtotal = calcular_subtotal(productos)
descuento = obtener_descuento(tipo_cliente)
impuesto = obtener_impuesto(pais)
total = subtotal * (1 - descuento)
total = total * (1 + impuesto)
total = aplicar_envio(total)
total = round(total, 2)
historial.append(total)
if guardar:
guardar_total(total)
if mostrar:
mostrar_total(total)
return total
Aún hay efectos secundarios, pero ya están más visibles.
def guardar_total(total, ruta="ultimo_pedido.txt"):
with open(ruta, "w", encoding="utf-8") as archivo:
archivo.write(str(total))
def mostrar_total(total):
print(f"Total: {total:.2f}")
Ahora el cálculo puede separarse del guardado y la presentación en una siguiente iteración.
Una alternativa más clara es que el cálculo no reciba guardar ni mostrar.
def calcular_total_pedido(productos, tipo_cliente, pais):
subtotal = calcular_subtotal(productos)
descuento = obtener_descuento(tipo_cliente)
impuesto = obtener_impuesto(pais)
total = subtotal * (1 - descuento)
total = total * (1 + impuesto)
total = aplicar_envio(total)
return round(total, 2)
Luego una función de orquestación decide si guarda o muestra.
El comportamiento anterior ignoraba datos inválidos. Si el negocio decide que eso no es aceptable, este cambio ya sería funcional y debe hacerse separado.
def validar_producto(producto):
if "precio" not in producto:
raise ValueError("Falta el precio")
if "cant" not in producto:
raise ValueError("Falta la cantidad")
if producto["cant"] <= 0:
raise ValueError("La cantidad debe ser positiva")
Este cambio requiere actualizar pruebas porque modifica el comportamiento frente a datos inválidos.
DESCUENTO_VIP = 0.15
DESCUENTO_REGULAR = 0.05
IMPUESTOS = {"AR": 0.21, "UY": 0.22}
IMPUESTO_PREDETERMINADO = 0.19
LIMITE_ENVIO_GRATIS = 10000
COSTO_ENVIO = 1500
def calcular_subtotal(productos):
return sum(
producto["precio"] * producto["cant"]
for producto in productos
if producto["cant"] > 0
)
def obtener_descuento(tipo_cliente):
if tipo_cliente == "vip":
return DESCUENTO_VIP
if tipo_cliente == "regular":
return DESCUENTO_REGULAR
return 0
def obtener_impuesto(pais):
return IMPUESTOS.get(pais, IMPUESTO_PREDETERMINADO)
def aplicar_envio(total):
if total < LIMITE_ENVIO_GRATIS:
return total + COSTO_ENVIO
return total
def calcular_total_pedido(productos, tipo_cliente, pais):
subtotal = calcular_subtotal(productos)
total = subtotal * (1 - obtener_descuento(tipo_cliente))
total = total * (1 + obtener_impuesto(pais))
return round(aplicar_envio(total), 2)
Cuando eliminas el alias y las banderas, actualiza las pruebas:
from pedidos import calcular_total_pedido
def test_cliente_vip_argentina():
productos = [
{"precio": 3000, "cant": 2},
{"precio": 1500, "cant": 1},
]
assert calcular_total_pedido(productos, "vip", "AR") == 9213.75
Ejecuta el flujo completo:
python -m isort src tests
python -m black src tests
python -m ruff check src tests
python -m mypy src
python -m pytest
Si alguna herramienta falla, corrige el problema más pequeño posible y vuelve a ejecutar.
En el README.md, registra una breve síntesis:
# Calidad Integrador
Proyecto de práctica para analizar y mejorar code smells en Python.
Mejoras realizadas:
- Nombres más expresivos.
- Constantes para reglas de negocio.
- Separación de subtotal, descuento, impuesto y envío.
- Eliminación de banderas booleanas en el cálculo principal.
- Pruebas de caracterización para proteger comportamiento.
Antes de dar por terminado el caso, revisa:
En este curso recorrimos criterios prácticos para mejorar la calidad de código en Python: nombres, estilo, herramientas, code smells, funciones, clases, dataclasses, type hints, módulos, pruebas y métricas.
La idea central es simple: el código de calidad no solo funciona hoy, también permite entender, cambiar y verificar mañana. Esa es la diferencia entre escribir una solución momentánea y construir software mantenible.