Saltar al contenido

# conceptos-avanzados-en-python

Conceptos Avanzados en Python

Volver a la portada del curso
Lectura (Markdown)

Lectura Final: Buenas Prácticas

Sección 4 · Proyecto Final de Python Intermedio

Lectura final: buenas prácticas

La herramienta ventas ya lee el CSV, descarta las 19 filas dañadas y calcula una venta total de 718890.0. Que funcione hoy no garantiza que siga funcionando cuando alguien agregue un producto o cambie el formato del archivo. Las buenas prácticas de esta lectura tienen un objetivo concreto: que el código se lea fácil, que los errores aparezcan antes de ejecutarlo y que cada cambio se pueda comprobar en segundos.

Estilo: PEP 8 y ruff

PEP 8 es la guía de estilo oficial de Python: nombres en snake_case para funciones y variables, CapWords para clases, MAYUSCULAS para constantes como PRODUCTOS, cuatro espacios de sangría e imports al inicio del archivo. Nadie la revisa a mano: se usa una herramienta. Ruff es un linter y formateador escrito en Rust que reúne en un solo programa las reglas de Flake8, isort y Black.

$ ruff check .          # busca errores y malas prácticas
$ ruff check --fix .    # corrige los que tienen arreglo seguro
$ ruff format .         # da formato al estilo de Black (líneas de 88)

En la versión 0.16, la configuración por defecto ya incluye reglas que detectan errores reales: F401 (import sin usar), B006 (argumento mutable por defecto) y E722 (except desnudo). Otras, como F403 (import *), hay que activarlas en pyproject.toml con [tool.ruff.lint] y extend-select = ["F"].

Type hints y docstrings

Las anotaciones de tipo no cambian lo que hace el programa, pero documentan la intención y permiten que el editor o un verificador como mypy detecten un error antes de ejecutar. El docstring (PEP 257) explica qué hace la función, qué recibe y qué excepciones lanza:

def venta_por_ciudad(pedidos: list[Pedido]) -> dict[str, float]:
    """Suma el importe de los pedidos agrupado por ciudad.

    Args:
        pedidos: pedidos ya validados.

    Returns:
        Un diccionario ciudad -> venta, redondeada a 2 decimales.
    """
    totales: defaultdict[str, float] = defaultdict(float)
    for p in pedidos:
        totales[p.ciudad] += p.importe
    return {ciudad: round(v, 2) for ciudad, v in totales.items()}

Con los 981 pedidos válidos, la función devuelve 158538.0 para CDMX y 118769.5 para Santiago. Anota primero las funciones públicas: son las que otras partes del programa llaman.

Pruebas con pytest

Los assert sueltos del notebook se convierten en pruebas automáticas. pytest ejecuta los archivos test_*.py o *_test.py y, dentro de ellos, las funciones que empiezan con test. Se escriben con assert normal, y pytest.raises comprueba que un error ocurra:

import pytest

from ventas.modelos import FilaInvalidaError, Pedido

FILA = {"id": "1", "fecha": "2026-09-11", "cliente": "C039",
        "ciudad": "Santiago", "producto": "Mouse", "cantidad": "1"}


def test_importe():
    assert Pedido.desde_fila(FILA, 2).importe == 349.5


def test_cantidad_negativa():
    with pytest.raises(FilaInvalidaError, match="no positiva"):
        Pedido.desde_fila(FILA | {"cantidad": "-1"}, 111)

Al correr pytest -q, la salida es 2 passed. Prueba las cifras que puedes calcular a mano y, sobre todo, los casos de error: una prueba que solo cubre el camino feliz no detecta la regresión que importa.

Entornos virtuales: venv y uv

Cada proyecto necesita su propio entorno virtual, para que sus dependencias no choquen con las de otros proyectos ni con el Python del sistema. La biblioteca estándar trae venv; uv hace lo mismo y además administra dependencias y el archivo de bloqueo uv.lock:

$ python -m venv .venv             # entorno con la biblioteca estándar
$ .venv\Scripts\activate           # Windows (en Linux/macOS: source .venv/bin/activate)

$ uv init --package ventas         # proyecto nuevo con src/ y [project.scripts]
$ uv add --dev pytest              # dependencia de desarrollo; crea .venv y uv.lock
$ uv run pytest -q                 # ejecuta dentro del entorno del proyecto

Agrega .venv al .gitignore: el entorno se reconstruye a partir de pyproject.toml y uv.lock, que sí van al repositorio.

Estructura del proyecto

ventas/
├── pyproject.toml
├── uv.lock
├── README.md
├── src/
│   └── ventas/
│       ├── __init__.py
│       ├── modelos.py     # Pedido, FilaInvalidaError
│       ├── lectura.py     # leer_pedidos
│       ├── reporte.py     # resúmenes por producto, ciudad y cliente
│       └── cli.py         # argparse y main()
└── tests/
    └── test_modelos.py

El diseño src/ obliga a instalar el paquete para importarlo, así que las pruebas usan el mismo código que usará quien lo instale, y no una copia que funcionó por accidente desde la carpeta actual.

Trampas comunes

  • Argumento mutable por defecto: def registrar(error, errores=[]) comparte la lista entre llamadas. Usa None y crea la lista dentro de la función.
  • except: desnudo: atrapa BaseException, incluido KeyboardInterrupt, así que oculta errores y hasta impide detener el programa con Ctrl+C. Atrapa la excepción concreta, como FilaInvalidaError.
  • from collections import *: no deja ver de dónde sale cada nombre y puede pisar otros sin aviso. Importa lo que usas: from collections import Counter, defaultdict.
  • Concatenar texto con + y str(): las f-strings son más legibles y controlan el formato.
  • Armar rutas con + "/" +: pathlib une rutas con / y funciona igual en Windows y Linux.
from pathlib import Path

ruta = Path("datos") / "pedidos.csv"
print(ruta.stem, ruta.suffix)            # pedidos .csv
print(ruta.with_suffix(".json").name)    # pedidos.json

total = 718890.0
print(f"Venta total: ${total:,.2f}")     # Venta total: $718,890.00
print(f"{'CDMX':<10}{158538.0:>12,.1f}")  # CDMX         158,538.0
print(f"{total=}")                       # total=718890.0
print(f"{19 / 1000:.1%} descartadas")    # 1.9% descartadas

Ninguna de estas prácticas exige reescribir el proyecto. Un orden razonable: activa ruff en el editor, escribe una prueba por cada cifra del reporte y anota las funciones públicas. Cada paso se puede hacer en una tarde y deja el código mejor que antes.