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.
-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 faltaassertEqualni heredar de nada: pytest reescribe elassertpara darte un informe detallado. pytest.approxpara los decimales, por lo que vimos en la lección 3: compararfloatpor igualdad exacta es pedir un disgusto.pytest.raisescomprueba el error, y conmatch=también el mensaje. Es la mitad del trabajo que la gente se salta.@parametrizeejecuta 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.