Refactorizar con seguridad no depende solo de leer bien el código. También necesitamos herramientas que nos avisen rápido cuando rompemos comportamiento, dejamos código sin cubrir, introducimos errores de estilo o usamos tipos de manera incoherente.
En este tema usaremos pytest, coverage, ruff, black y mypy como apoyo práctico para refactorizar un módulo Python en pasos pequeños.
Estas herramientas no reemplazan el criterio de diseño, pero hacen visible el riesgo.
En un proyecto real conviene declarar dependencias de desarrollo. Para esta práctica, puedes instalarlas en tu entorno virtual:
python -m pip install pytest coverage ruff black mypy
La instalación no cambia el código de producción. Solo agrega herramientas para trabajar con más seguridad.
Crea el archivo src/reportes_herramientas.py:
def generar_reporte(ventas):
total = 0
cantidad = 0
clientes = []
lineas = []
for venta in ventas:
if venta["estado"] == "cancelada":
continue
total = total + venta["importe"]
cantidad = cantidad + 1
if venta["cliente"] not in clientes:
clientes.append(venta["cliente"])
if cantidad == 0:
promedio = 0
else:
promedio = total / cantidad
lineas.append("REPORTE DE VENTAS")
lineas.append(f"Total: {total}")
lineas.append(f"Cantidad: {cantidad}")
lineas.append(f"Promedio: {promedio}")
lineas.append(f"Clientes: {', '.join(clientes)}")
return "\n".join(lineas)
El módulo funciona, pero mezcla filtrado, cálculo y formato. Lo refactorizaremos sin perder comportamiento.
Crea tests/test_reportes_herramientas.py:
from src.reportes_herramientas import generar_reporte
def test_genera_reporte_de_ventas_activas():
ventas = [
{"cliente": "Ana", "importe": 1000, "estado": "pagada"},
{"cliente": "Luis", "importe": 500, "estado": "cancelada"},
{"cliente": "Ana", "importe": 3000, "estado": "pagada"},
]
reporte = generar_reporte(ventas)
assert reporte == (
"REPORTE DE VENTAS\n"
"Total: 4000\n"
"Cantidad: 2\n"
"Promedio: 2000.0\n"
"Clientes: Ana"
)
def test_genera_reporte_sin_ventas_activas():
ventas = [{"cliente": "Ana", "importe": 1000, "estado": "cancelada"}]
reporte = generar_reporte(ventas)
assert reporte == (
"REPORTE DE VENTAS\n"
"Total: 0\n"
"Cantidad: 0\n"
"Promedio: 0\n"
"Clientes: "
)
python -m pytest tests/test_reportes_herramientas.py
La cobertura no garantiza calidad, pero ayuda a detectar zonas que podrían romperse sin aviso:
python -m coverage run -m pytest tests/test_reportes_herramientas.py
python -m coverage report -m
Si una rama importante no está cubierta, conviene agregar una prueba antes de refactorizarla.
ruff puede encontrar imports sin uso, variables innecesarias, comparaciones problemáticas y otros detalles que complican el mantenimiento:
python -m ruff check src tests
Algunos problemas pueden corregirse automáticamente:
python -m ruff check src tests --fix
Después de aplicar correcciones automáticas, ejecuta nuevamente las pruebas.
Antes de tocar lógica, es útil dejar el formato estable. Así los cambios del refactoring quedan separados de los cambios cosméticos:
python -m black src tests
Luego confirma que el formato no cambió comportamiento:
python -m pytest
Los tipos ayudan a documentar expectativas. Podemos empezar con alias simples:
from typing import TypedDict
class Venta(TypedDict):
cliente: str
importe: float
estado: str
def generar_reporte(ventas: list[Venta]) -> str:
...
No necesitamos tipar todo el proyecto de una vez. Es suficiente comenzar por el módulo que vamos a refactorizar.
Una vez agregados type hints, ejecutamos la revisión de tipos:
python -m mypy src/reportes_herramientas.py
Si mypy marca muchos errores, no los corrijas todos sin criterio. Prioriza los que ayudan a entender contratos reales del módulo.
Extraemos el filtrado para darle nombre a la regla:
def obtener_ventas_activas(ventas: list[Venta]) -> list[Venta]:
return [venta for venta in ventas if venta["estado"] != "cancelada"]
Después del cambio, ejecutamos la verificación mínima:
python -m pytest tests/test_reportes_herramientas.py
python -m ruff check src tests
El cálculo de total, cantidad, promedio y clientes puede ir a una función separada:
def calcular_resumen(ventas: list[Venta]) -> dict[str, object]:
total = sum(venta["importe"] for venta in ventas)
cantidad = len(ventas)
promedio = total / cantidad if cantidad else 0
clientes = []
for venta in ventas:
if venta["cliente"] not in clientes:
clientes.append(venta["cliente"])
return {
"total": total,
"cantidad": cantidad,
"promedio": promedio,
"clientes": clientes,
}
Esta extracción permite probar el cálculo sin depender del texto final del reporte.
El formato del reporte también puede tener una función propia:
def formatear_reporte(resumen: dict[str, object]) -> str:
return "\n".join(
[
"REPORTE DE VENTAS",
f"Total: {resumen['total']}",
f"Cantidad: {resumen['cantidad']}",
f"Promedio: {resumen['promedio']}",
f"Clientes: {', '.join(resumen['clientes'])}",
]
)
Luego el punto de entrada queda como una coordinación simple.
from typing import TypedDict
class Venta(TypedDict):
cliente: str
importe: float
estado: str
class ResumenVentas(TypedDict):
total: float
cantidad: int
promedio: float
clientes: list[str]
def obtener_ventas_activas(ventas: list[Venta]) -> list[Venta]:
return [venta for venta in ventas if venta["estado"] != "cancelada"]
def calcular_resumen(ventas: list[Venta]) -> ResumenVentas:
total = sum(venta["importe"] for venta in ventas)
cantidad = len(ventas)
promedio = total / cantidad if cantidad else 0
clientes: list[str] = []
for venta in ventas:
if venta["cliente"] not in clientes:
clientes.append(venta["cliente"])
return {
"total": total,
"cantidad": cantidad,
"promedio": promedio,
"clientes": clientes,
}
def formatear_reporte(resumen: ResumenVentas) -> str:
return "\n".join(
[
"REPORTE DE VENTAS",
f"Total: {resumen['total']}",
f"Cantidad: {resumen['cantidad']}",
f"Promedio: {resumen['promedio']}",
f"Clientes: {', '.join(resumen['clientes'])}",
]
)
def generar_reporte(ventas: list[Venta]) -> str:
ventas_activas = obtener_ventas_activas(ventas)
resumen = calcular_resumen(ventas_activas)
return formatear_reporte(resumen)
Las pruebas originales deben seguir pasando. Además, podemos sumar pruebas enfocadas para las nuevas funciones:
from src.reportes_herramientas import calcular_resumen, obtener_ventas_activas
def test_obtiene_ventas_activas():
ventas = [
{"cliente": "Ana", "importe": 1000.0, "estado": "pagada"},
{"cliente": "Luis", "importe": 500.0, "estado": "cancelada"},
]
assert obtener_ventas_activas(ventas) == [
{"cliente": "Ana", "importe": 1000.0, "estado": "pagada"}
]
def test_calcula_resumen_de_ventas():
ventas = [
{"cliente": "Ana", "importe": 1000.0, "estado": "pagada"},
{"cliente": "Ana", "importe": 3000.0, "estado": "pagada"},
]
assert calcular_resumen(ventas) == {
"total": 4000.0,
"cantidad": 2,
"promedio": 2000.0,
"clientes": ["Ana"],
}
Al terminar una refactorización, ejecuta una secuencia completa:
python -m black src tests
python -m ruff check src tests
python -m mypy src
python -m coverage run -m pytest
python -m coverage report -m
El orden puede variar, pero la idea es combinar formato, linting, tipos, pruebas y cobertura.
Para no olvidar pasos, puedes crear un archivo verificar.ps1 en Windows:
python -m black src tests
python -m ruff check src tests
python -m mypy src
python -m coverage run -m pytest
python -m coverage report -m
El script no debe reemplazar el criterio del desarrollador, pero reduce omisiones mecánicas.
Una herramienta puede señalar un problema real o un caso que requiere decisión. Por ejemplo, ruff puede sugerir simplificar una expresión, pero si la versión explícita ayuda al alumno o al equipo, puede ser razonable mantenerla.
La regla práctica es simple: corrige lo que reduzca riesgo o aumente claridad. No hagas cambios automáticos que no entiendes.
Toma un módulo pequeño de un proyecto Python y realiza esta secuencia:
black y ruff.mypy.Antes de continuar, verifica que puedes explicar estos puntos:
En este tema usamos herramientas para refactorizar con más seguridad. Las pruebas protegieron comportamiento, la cobertura mostró zonas de riesgo, el formateo estabilizó el estilo, el linting detectó problemas comunes y los tipos hicieron más explícitos los contratos.
En el próximo tema trabajaremos estrategias para refactorizar código heredado en pasos pequeños.