2. Preparar un proyecto Python para refactorizar con seguridad

2.1 Objetivo del tema

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.

Objetivo práctico: dejar listo un proyecto mínimo donde podamos refactorizar código y verificar el comportamiento con un solo comando.

2.2 Por qué preparar el proyecto antes de tocar el código

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:

  • Una estructura de archivos que se entienda.
  • Pruebas automatizadas que documenten el comportamiento actual.
  • Comandos repetibles para ejecutar verificaciones siempre de la misma manera.

2.3 Crear la carpeta del proyecto

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.

2.4 Crear y activar un entorno virtual

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.

2.5 Instalar herramientas de desarrollo

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

2.6 Crear el archivo pyproject.toml

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.

2.7 Agregar código heredado de práctica

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.

2.8 Escribir las primeras 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.

2.9 Ejecutar las pruebas

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.

2.10 Corregir una prueba mal planteada

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.

2.11 Ejecutar revisión de estilo con Ruff

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

2.12 Formatear con Black

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.

2.13 Revisar tipos con mypy

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.

2.14 Crear un comando de verificación

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.

2.15 Primer refactoring seguro

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.

2.16 Guardar el punto de partida

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.

2.17 Ejercicio propuesto

Completa estas tareas sobre el proyecto refactoring_demo:

  • Agrega una prueba para un pedido premium con cupón DESC10 y envío normal.
  • Agrega una prueba para un pedido regular con envío express.
  • Ejecuta python -m pytest y confirma que todas las pruebas pasan.
  • Ejecuta ruff check . y revisa si informa problemas.
  • Ejecuta black . y luego vuelve a correr las pruebas.

2.18 Una posible solución para las pruebas nuevas

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.

2.19 Lista de verificación

Antes de pasar al próximo tema, verifica que puedes explicar y ejecutar estos puntos:

  • Crear una estructura con carpetas src y tests.
  • Crear y activar un entorno virtual.
  • Instalar herramientas de desarrollo del proyecto.
  • Configurar pytest, ruff, black y mypy desde pyproject.toml.
  • Escribir pruebas que documenten el comportamiento actual.
  • Ejecutar verificaciones antes y después de cada refactoring.

2.20 Conclusión

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.