pyproject.toml es el archivo central para declarar cómo se construye un proyecto Python, qué metadatos lo describen y cómo funcionan las herramientas compatibles. [build-system] elige el backend; [project] define identidad, compatibilidad y dependencias de ejecución; [tool.*] almacena opciones específicas. Las aplicaciones suelen priorizar despliegues reproducibles y las librerías, compatibilidad amplia y correctamente declarada.
Esta guía aborda configuración, no la subida de artefactos a un índice. Para ese flujo separado, consulta cómo publicar un paquete en PyPI. También conviene conocer módulos y paquetes Python, entornos virtuales y gestión de dependencias con pip.
Qué estandariza pyproject.toml
Los proyectos antiguos repartían información entre setup.py, setup.cfg, archivos requirements y configuraciones separadas. pyproject.toml ofrece puntos comunes, pero no todas sus tablas son estándares de packaging. [project] sigue una especificación compartida; [tool.ruff] pertenece exclusivamente a Ruff.
TOML contiene claves, listas y tablas. Los comentarios comienzan con #, los textos llevan comillas y los nombres con puntos indican niveles. Un archivo puede ser TOML válido y aun así tener una opción inexistente. Valídalo ejecutando el build y las herramientas reales.
La guía oficial de PyPA para escribir pyproject.toml presenta decisiones prácticas; la especificación de pyproject.toml define el contrato. La PEP 518 explica los requisitos de build aislados.
build-system define la construcción
Una configuración con setuptools es breve:
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
requires enumera software necesario para ejecutar el backend en un entorno aislado. Flask, requests y librerías usadas por la aplicación no van allí, sino en [project].dependencies. El frontend crea un entorno, instala requisitos de construcción y llama al backend para producir wheel o sdist.
Otros backends necesitan valores diferentes. Sigue la documentación de uno en vez de mezclar fragmentos incompatibles. El mínimo debe corresponder a las funciones usadas. Fijar exactamente todas las versiones dificulta mantenimiento, pero admitir una versión demasiado antigua puede romper la interpretación.
Un conjunto de scripts que nunca se instala quizá no requiera backend. Sin embargo, empaquetar una aplicación evita imports dependientes del directorio actual y acerca las pruebas al despliegue. Un layout src impide importar accidentalmente desde la raíz.
project reúne identidad y requisitos
Una librería pequeña puede declarar:
[project]
name = "texto-claro"
version = "0.3.0"
description = "Utilidades pequeñas para normalizar texto"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{ name = "Equipo Texto Claro" }]
dependencies = ["regex>=2024.5"]
[project.optional-dependencies]
cli = ["rich>=13"]
[project.scripts]
texto-claro = "texto_claro.cli:main"
name identifica la distribución y puede diferir del paquete importable. requires-python impide instalaciones incompatibles. dependencies solo incluye requisitos de ejecución. Los extras ofrecen capacidades opcionales instalables con pip install 'texto-claro[cli]'. Un script enlaza un comando con una función importable.
Los metadatos pueden ser estáticos o aparecer en dynamic. Prefiere valores estáticos porque se inspeccionan sin ejecutar lógica del backend. Si la versión procede del código o control de versiones, declara dynamic = ["version"] y configura el método según el backend. Un mismo campo no puede ser estático y dinámico.
Aplicaciones y librerías no fijan igual
Una librería debe aceptar un rango útil y probar sus límites relevantes. Fijar requests==... en dependencias obliga a consumidores a resolver conflictos innecesarios. Indica un mínimo cuando utilizas una función concreta y un máximo solo ante incompatibilidad conocida.
Una aplicación controla el entorno final y normalmente necesita una resolución exacta. pyproject.toml puede expresar rangos directos; el lock del gestor conserva versiones, hashes y dependencias transitivas. [project].dependencies no es un lock.
pytest y Ruff son herramientas de desarrollo. Cuando el instalador soporte grupos estándar:
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
Los grupos sirven al desarrollo y CI; no se convierten en extras publicados. Confirma soporte. Si el gestor no los reconoce, utiliza su mecanismo documentado y no inventes una tabla similar.
Configuración bajo tool
Cada herramienta controla su esquema. Una base sencilla:
[tool.pytest.ini_options]
addopts = "-ra --strict-markers"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "B"]
No supongas que opciones parecidas funcionan igual en Ruff, mypy y coverage. Lee cada documentación y conserva únicamente decisiones del proyecto. Copiar todos los defaults agrega ruido y puede perpetuar valores obsoletos.
La guía de pruebas con pytest explica fixtures y organización. En CI, prueba la instalación y los comandos verdaderos siguiendo GitHub Actions para Python.
Validar el proyecto en un entorno limpio
Leer el TOML no basta para demostrar que la configuración funciona. Una prueba útil reproduce el recorrido de quien consume el proyecto: construir las distribuciones, instalar el wheel e importar el paquete fuera del árbol del repositorio. Instala el frontend de build en el entorno de desarrollo y ejecuta:
python -m pip install build
python -m build
python -m pip install --force-reinstall dist/*.whl
python -c "import texto_claro; print(texto_claro.__name__)"
El build debe producir un wheel y, normalmente, una distribución de código fuente. Revisa su contenido antes de publicar. Los módulos, la licencia, el README y los archivos de datos necesarios deben estar incluidos; cachés, secretos, artefactos locales y pruebas ajenas no deberían aparecer. Un wheel que parece funcionar desde el repositorio pero falla tras instalarlo suele revelar una detección incorrecta de paquetes o una dependencia accidental del directorio actual.
Para una librería, prueba también la instalación desde la distribución de código fuente. Ese recorrido obliga al frontend a reconstruir el wheel y descubre requisitos de build ausentes. Para una aplicación, crea un entorno vacío con la versión mínima de Python declarada, instala el proyecto mediante el procedimiento documentado y ejecuta al menos el comando inicial y las pruebas esenciales.
Son tres capas distintas. Un parser TOML detecta errores de sintaxis. El backend comprueba si entiende las tablas de build y metadatos. La instalación y la ejecución confirman que el artefacto representa el proyecto real. Mantener estas comprobaciones en CI es más fiable que probar solo una instalación editable, porque ese modo puede exponer archivos fuente que el wheel omitiría.
Trampas frecuentes
- Colocar dependencias de la aplicación en
[build-system].requires. - Fijar versiones exactas de una librería sin motivo concreto.
- Confundir un extra para usuarios con un grupo de desarrollo.
- Declarar el mismo campo estático y dinámico.
- Copiar tablas
[tool.*]sin comprender la herramienta. - Creer que
pyproject.tomlaporta un lock exacto. - Declarar versiones de Python que CI no prueba.
- Mezclar configuraciones de backends diferentes.
- Depender de imports que solo funcionan desde la raíz.
Checklist de revisión
- Backend y requisitos de construcción son correctos y suficientes.
- Los metadatos son estáticos cuando resulta práctico.
- Las dependencias de ejecución excluyen test y lint.
- Los extras representan capacidades opcionales para usuarios.
- Los grupos son internos y el instalador los soporta.
- La aplicación tiene una estrategia explícita de lock.
- La librería prueba rangos declarados de Python y dependencias.
- Cada tabla
toolsigue documentación actual. - Build, instalación y pruebas pasan en un entorno limpio.
Empieza con la configuración mínima. Añade opciones cuando exista una necesidad concreta, verifica en un entorno virtual vacío y separa construcción, ejecución y desarrollo. Esa división mantiene el archivo comprensible y evita trasladar decisiones de despliegue de una aplicación a los consumidores de una librería.