Antes de refactorizar código conviene preparar un entorno de trabajo que permita comprobar cambios con rapidez. Si cada verificación depende de ejecutar pasos manuales, revisar salidas a ojo o recordar comandos sueltos, el refactoring se vuelve lento y riesgoso.
En este tema construiremos un pequeño proyecto Python para practicar durante el curso. Dejaremos una estructura clara de carpetas, un entorno virtual, dependencias de desarrollo, pruebas con pytest y herramientas básicas para revisar formato, estilo y tipos.
Cuando el código existente es confuso, la tentación es empezar a cambiar nombres, separar funciones y mover archivos. El problema es que, sin una forma rápida de comprobar resultados, cada mejora puede introducir un error difícil de detectar.
La preparación inicial no tiene que ser compleja. Necesitamos tres cosas:
Para practicar, crea una carpeta llamada refactoring_demo. Dentro de ella separaremos el código de aplicación y las pruebas.
mkdir refactoring_demo
cd refactoring_demo
mkdir src
mkdir tests
La estructura inicial quedará así:
refactoring_demo/
src/
tests/
Durante el curso usaremos ejemplos pequeños, pero conviene practicar con una estructura similar a la que se utiliza en proyectos reales.
El entorno virtual permite instalar herramientas del proyecto sin mezclarlas con otras instalaciones de Python del equipo.
En Windows puedes ejecutar:
python -m venv .venv
.venv\Scripts\activate
En Linux o macOS puedes ejecutar:
python -m venv .venv
source .venv/bin/activate
Una vez activado, la terminal suele mostrar (.venv) al comienzo de la línea. Eso indica que las instalaciones se harán dentro del entorno del proyecto.
Para este curso usaremos herramientas conocidas del ecosistema Python:
pytest para ejecutar pruebas.ruff para detectar problemas comunes de estilo y calidad.black para formatear código automáticamente.mypy para revisar tipos cuando agreguemos anotaciones.python -m pip install pytest ruff black mypy
También podemos guardar estas dependencias en un archivo para que otra persona pueda instalar el mismo entorno.
python -m pip freeze > requirements-dev.txt
El archivo pyproject.toml permite centralizar configuración de herramientas. Para empezar, crea este archivo en la raíz del proyecto:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[tool.black]
line-length = 88
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "B", "I"]
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
strict = false
No hace falta memorizar cada opción. Lo importante es que el proyecto tenga reglas explícitas y que todos puedan ejecutar las mismas verificaciones.
Ahora crearemos un módulo con código que funciona, pero que será incómodo de mantener. Este será nuestro punto de partida.
Crea el archivo src/pedidos.py:
def procesar_pedido(pedido):
total = 0
for item in pedido["items"]:
total = total + item["precio"] * item["cantidad"]
if pedido["cliente"] == "premium":
total = total * 0.9
if pedido["cupon"] == "DESC10":
total = total - 10
if pedido["envio"] == "express":
total = total + 1500
else:
total = total + 500
if total < 0:
total = 0
return total
Este código nos servirá para practicar. Todavía no lo vamos a mejorar. Primero vamos a documentar su comportamiento actual con pruebas.
Crea el archivo tests/test_pedidos.py:
from pedidos import procesar_pedido
def test_procesa_pedido_premium_con_envio_express():
pedido = {
"cliente": "premium",
"cupon": "",
"envio": "express",
"items": [
{"precio": 1000, "cantidad": 2},
{"precio": 500, "cantidad": 1},
],
}
assert procesar_pedido(pedido) == 3750
def test_procesa_pedido_regular_con_cupon_y_envio_normal():
pedido = {
"cliente": "regular",
"cupon": "DESC10",
"envio": "normal",
"items": [
{"precio": 100, "cantidad": 1},
],
}
assert procesar_pedido(pedido) == 590
def test_no_devuelve_total_negativo():
pedido = {
"cliente": "regular",
"cupon": "DESC10",
"envio": "normal",
"items": [],
}
assert procesar_pedido(pedido) == 490
Estas pruebas expresan el comportamiento que existe ahora. Si más adelante decidimos cambiar reglas de negocio, será otra tarea. Por ahora queremos refactorizar sin cambiar resultados.
Desde la raíz del proyecto ejecuta:
python -m pytest
Si todo está configurado correctamente, python -m pytest encontrará las pruebas dentro de tests y podrá importar el módulo desde src.
El resultado esperado es que pasen tres pruebas. Esa salida nos indica que el proyecto ya tiene una primera red de seguridad.
La tercera prueba del bloque anterior tiene un nombre sospechoso: dice que el total no debe ser negativo, pero el resultado esperado es 490. Eso ocurre porque el costo de envío normal se suma después del cupón.
Una prueba con nombre impreciso puede confundir durante un refactoring. Podemos mejorar el nombre sin cambiar la expectativa:
def test_aplica_cupon_y_envio_normal_cuando_no_hay_items():
pedido = {
"cliente": "regular",
"cupon": "DESC10",
"envio": "normal",
"items": [],
}
assert procesar_pedido(pedido) == 490
Este también es un refactoring, pero de la prueba: mejora la claridad sin cambiar lo que se verifica.
Antes de modificar el código de aplicación, podemos pedirle a ruff que revise problemas simples.
ruff check .
Si la herramienta informa problemas, conviene leerlos con calma. No todos exigen un cambio inmediato, pero muchos ayudan a detectar imports sin usar, nombres inválidos, errores simples o detalles que conviene limpiar.
En algunos casos ruff puede aplicar correcciones automáticas:
ruff check . --fix
El formateo automático reduce discusiones de estilo y deja que nos concentremos en el diseño. Ejecuta:
black .
Después del formateo, vuelve a ejecutar las pruebas:
python -m pytest
El formato no debería cambiar el comportamiento. Si una prueba falla después de formatear, es una señal de que algo raro ocurrió y conviene revisar antes de seguir.
Al comienzo del curso no exigiremos tipado estricto, pero es útil tener mypy disponible. Podemos ejecutarlo sobre la carpeta src:
mypy src
Más adelante agregaremos type hints para que el código comunique mejor qué datos espera y qué devuelve. Por ahora alcanza con saber que la herramienta forma parte del entorno de seguridad.
Cuando refactorizamos, repetimos muchas veces las mismas verificaciones. Podemos definir una secuencia simple de comandos:
ruff check .
black --check .
mypy src
python -m pytest
Durante una práctica manual puedes ejecutarlos por separado. En un proyecto más grande, estos comandos suelen integrarse en un archivo de tareas, en un script o en un pipeline de integración continua.
Ahora haremos un cambio pequeño en src/pedidos.py: renombrar la variable total por subtotal mientras todavía está acumulando los items. Este cambio ayuda a distinguir el valor inicial del total final.
def procesar_pedido(pedido):
subtotal = 0
for item in pedido["items"]:
subtotal = subtotal + item["precio"] * item["cantidad"]
total = subtotal
if pedido["cliente"] == "premium":
total = total * 0.9
if pedido["cupon"] == "DESC10":
total = total - 10
if pedido["envio"] == "express":
total = total + 1500
else:
total = total + 500
if total < 0:
total = 0
return total
Después del cambio, ejecuta python -m pytest. Si las pruebas pasan, el comportamiento observado se mantiene.
Antes de comenzar una serie de refactorizaciones conviene guardar un punto estable del proyecto. Si estás usando Git, puedes ejecutar:
git init
git add .
git commit -m "Preparar proyecto para practicar refactoring"
El control de versiones no reemplaza a las pruebas, pero permite volver a un estado conocido si una secuencia de cambios no sale bien.
Completa estas tareas sobre el proyecto refactoring_demo:
DESC10 y envío normal.python -m pytest y confirma que todas las pruebas pasan.ruff check . y revisa si informa problemas.black . y luego vuelve a correr las pruebas.Puedes agregar estas pruebas al archivo tests/test_pedidos.py:
def test_procesa_pedido_premium_con_cupon_y_envio_normal():
pedido = {
"cliente": "premium",
"cupon": "DESC10",
"envio": "normal",
"items": [
{"precio": 1000, "cantidad": 2},
],
}
assert procesar_pedido(pedido) == 2290
def test_procesa_pedido_regular_con_envio_express():
pedido = {
"cliente": "regular",
"cupon": "",
"envio": "express",
"items": [
{"precio": 800, "cantidad": 3},
],
}
assert procesar_pedido(pedido) == 3900
Estas pruebas aumentan la confianza antes de seguir refactorizando. Cubren combinaciones distintas de descuento, cupón y envío.
Antes de pasar al próximo tema, verifica que puedes explicar y ejecutar estos puntos:
src y tests.pytest, ruff, black y mypy desde pyproject.toml.En este tema preparamos un proyecto Python para refactorizar con más seguridad. Creamos una estructura simple, instalamos herramientas, agregamos pruebas y ejecutamos verificaciones repetibles.
En el próximo tema trabajaremos con pruebas de caracterización, una técnica especialmente útil cuando debemos modificar código existente sin conocer todos sus detalles internos.