# conceptos-avanzados-en-python
Conceptos Avanzados en Python
Volver a la portada del cursoLectura 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. UsaNoney crea la lista dentro de la función. except:desnudo: atrapaBaseException, incluidoKeyboardInterrupt, así que oculta errores y hasta impide detener el programa con Ctrl+C. Atrapa la excepción concreta, comoFilaInvalidaError.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
+ystr(): las f-strings son más legibles y controlan el formato. - Armar rutas con
+ "/" +:pathlibune 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.