Un proyecto de verdad

La última lección junta todo lo anterior en un proyecto pequeño pero completo: estructura con src/, pyproject.toml, seis pruebas con pytest y un paquete instalable. Es el esqueleto que puedes copiar para el siguiente.

La estructura

el proyecto enteroinventario/
├── pyproject.toml              qué es el paquete y qué necesita
├── src/
│   └── inventario/
│       ├── __init__.py         qué exporta el paquete
│       ├── modelo.py           las dataclasses y la lógica
│       └── cli.py              el punto de entrada
└── tests/
    └── test_modelo.py

Lo importante es el src/. Sin él, al ejecutar las pruebas Python encuentra el paquete en la carpeta actual y nunca llegas a probar lo que de verdad se instala; con él, las pruebas solo funcionan si el paquete está bien instalado. Se llama src layout y es lo recomendado hoy.

pyproject.toml

[project]
name = "inventario"
version = "0.1.0"
description = "Un inventario mínimo"
requires-python = ">=3.12"
dependencies = []

[project.scripts]
inventario = "inventario.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
testpaths = ["tests"]

Un solo archivo sustituye al setup.py, al setup.cfg, al requirements.txt y a la configuración de las herramientas. Lo más interesante es [project.scripts]: esa línea hace que, al instalar, aparezca una orden llamada inventario en el sistema.

El código

@dataclass(frozen=True, slots=True)
class Producto:
    nombre: str
    precio: float
    stock: int = 0

    def __post_init__(self) -> None:
        if not self.nombre.strip():
            raise ValueError("el nombre no puede estar vacío")
        if self.precio < 0:
            raise ValueError(f"precio negativo: {self.precio}")

    @property
    def valor(self) -> float:
        return self.precio * self.stock

    @property
    def hay(self) -> bool:
        return self.stock > 0
@dataclass
class Inventario:
    productos: dict[str, Producto] = field(default_factory=dict)

    def buscar(self, nombre: str) -> Producto | None:
        return self.productos.get(nombre)

    def disponibles(self) -> list[Producto]:
        return [p for p in self.productos.values() if p.hay]

    @property
    def valor_total(self) -> float:
        return sum(p.valor for p in self.productos.values())

Ahí está medio curso junto: dataclasses con validación, anotaciones de tipo, field(default_factory=dict) en vez del mutable compartido, get devolviendo None, una comprensión y un generador dentro de sum.

Instalar y ejecutar

python3 -m venv .venv
source .venv/bin/activate
pip install -e .          # modo editable: los cambios se ven sin reinstalar
python3 -m inventario.cli gorra3 productos, 155.50 € en total
disponibles: ['gorra', 'mochila']
gorra -> Producto(nombre='gorra', precio=12.5, stock=7)
inventario pajarita3 productos, 155.50 € en total
disponibles: ['gorra', 'mochila']
pajarita -> no hay

La segunda es la orden que creó [project.scripts]: ya está en el PATH del entorno virtual, sin escribir python3 ni la ruta.

El -e de pip install -e . es lo que querrás el 100 % de las veces mientras desarrollas: instala un enlace a tu carpeta en vez de una copia, así que editas y pruebas sin reinstalar nada.

Pruebas con pytest

import pytest
from inventario import Producto, Inventario


def test_el_valor_es_precio_por_stock():
    assert Producto("gorra", 12.5, 7).valor == pytest.approx(87.5)


def test_rechaza_el_nombre_vacio():
    with pytest.raises(ValueError, match="el nombre no puede estar vacío"):
        Producto("  ", 1, 1)


@pytest.mark.parametrize("precio,stock", [(-1, 0), (1, -1)])
def test_rechaza_negativos(precio, stock):
    with pytest.raises(ValueError):
        Producto("x", precio, stock)


def test_buscar_devuelve_none_si_no_esta():
    assert Inventario().buscar("fantasma") is None
pytest -q......                                                                   [100%]
6 passed in 0.01s

Cuatro cosas de esas pruebas merecen un comentario:

  • Solo hay assert. No hace falta assertEqual ni heredar de nada: pytest reescribe el assert para darte un informe detallado.
  • pytest.approx para los decimales, por lo que vimos en la lección 3: comparar float por igualdad exacta es pedir un disgusto.
  • pytest.raises comprueba el error, y con match= también el mensaje. Es la mitad del trabajo que la gente se salta.
  • @parametrize ejecuta la misma prueba con varios datos. Por eso salen 6 pruebas de 5 funciones.

Y lo que sale cuando una falla, que es para lo que sirven:

pytestdef test_a_proposito():
>       assert Producto("x", 12.5, 7).valor == 100.0
E       AssertionError: assert 87.5 == 100.0
E        +  where 87.5 = Producto(nombre='x', precio=12.5, stock=7).valor

No dice solo «falló»: dice qué valía cada parte de la expresión. Eso es lo que hace que no necesites llenar la prueba de print.

Empaquetar

pip install build
python3 -m build --wheel
salidaSuccessfully built inventario-0.1.0-py3-none-any.whl
  inventario-0.1.0-py3-none-any.whl 2420 bytes

Un .whl es un zip con el paquete y sus metadatos. Se instala con pip install inventario-0.1.0-py3-none-any.whl en cualquier máquina, y se sube a PyPI con twine upload si quieres que lo instale todo el mundo.

Hasta aquí

Con esto tienes lo que hace falta para leer y escribir Python actual: el sistema de tipos, las colecciones, las comprensiones, las funciones con todas sus formas, las dataclasses, las excepciones, pathlib y un proyecto que se instala, se prueba y se empaqueta.

Lo que viene después depende de a qué te dediques: asyncio si haces red, pandas o polars si manejas datos, FastAPI o Django si haces web. Nada de eso es fácil sin lo de estas diez lecciones, y casi todo es asequible con ello.

Para… Orden
Crear el entorno python3 -m venv .venv
Instalar el proyecto pip install -e .
Probar pytest -q
Comprobar los tipos mypy src o ty check
Formatear y revisar ruff format y ruff check
Empaquetar python3 -m build

Todo el código de esta lección se ejecutó con Python 3.14 en un contenedor limpio antes de publicarla; las salidas y los errores están copiados de esa ejecución.