Guía de pyproject.toml: empaquetado, dependencias y herramientas
Traducción automática
Este artículo se tradujo automáticamente a partir de la versión original en inglés.
Los proyectos de Python más antiguos suelen repartir la configuración entre setup.py, setup.cfg, ficheros de requisitos, MANIFEST.in y ficheros aparte para las herramientas de desarrollo. pyproject.toml da a todos esos asuntos un lugar común, aunque no sustituye todos los ficheros del proyecto ni hace que cada sección forme parte de un único estándar.
Esta guía es para desarrolladores de Python que mantienen un paquete o una aplicación. Aprenderás qué contiene cada una de las cuatro tablas principales ([build-system], [project], [dependency-groups], [tool.*]), en qué se diferencian de un lockfile y de requirements.txt, qué backend de compilación elegir y cómo migrar un proyecto antiguo y crear uno nuevo con uv.
Un fichero, cuatro tablas
Tres estándares de empaquetado establecieron la estructura básica:
- PEP 518 define los requisitos del sistema de compilación.
- PEP 621 define los metadatos del proyecto.
- PEP 735 define los grupos de dependencias no publicados.
Los autores de herramientas también pueden reclamar un espacio de nombres bajo [tool]. Nada estandariza lo que va ahí: cada herramienta define sus propias claves y su comportamiento.
La figura divide el fichero de ejemplo de la sección siguiente en sus cuatro tablas y muestra qué programa lee cada una. Solo [project] llega a los usuarios, a través de los metadatos del wheel construido.
Este es un ejemplo de una biblioteca pequeña empaquetada:
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
[project]
name = "weather-client"
version = "0.1.0"
description = "A small weather API client"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
]
[project.optional-dependencies]
cli = ["rich>=13"]
[project.scripts]
weather = "weather_client.cli:main"
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
El fichero tiene cuatro listas de dependencias. dependencies enumera lo que el paquete necesita en tiempo de ejecución, optional-dependencies lo que los usuarios pueden elegir añadir, [dependency-groups] lo que los colaboradores instalan en local y [build-system].requires lo que el backend necesita para compilar.
[build-system]: cómo el código fuente se convierte en una distribución
Un frontend de compilación como python -m build, pip o uv invoca a un backend de compilación. El backend decide cómo el árbol de código fuente se convierte en un sdist o un wheel y qué ficheros entran en esos artefactos. En el ejemplo, build-backend nombra a hatchling y requires enumera lo que el frontend instala para ejecutarlo.
Un sdist es un archivo de código fuente que el instalador construye antes de instalarlo; un wheel contiene los ficheros listos para instalar.
La compilación sigue este orden. El frontend lee [build-system], instala requires en un entorno de compilación temporal y ejecuta el backend. El backend lee [project] y escribe el sdist y el wheel.
requires contiene las dependencias necesarias para ejecutar el backend en su entorno de compilación aislado. No es la lista de dependencias de ejecución de tu paquete.
Declara un sistema de compilación cuando el proyecto produzca una distribución o necesite que su propio código se instale como paquete. Un proyecto que no es un paquete también puede usar pyproject.toml; la PEP 735 permite incluso un fichero que contenga solo grupos de dependencias. Sin un sistema de compilación, uv no instala el proyecto en su propio entorno: importar tu propio código solo funciona desde la raíz del repositorio y uv no crea los comandos de [project.scripts]. Define tool.uv.package = true o añade un sistema de compilación cuando necesites alguna de las dos cosas (empaquetado de proyectos en uv).
Elegir un backend
Elige el backend según lo que tenga que hacer la compilación:
| Backend | Úsalo cuando |
|---|---|
uv_build | El proyecto es Python puro, se gestiona con uv y no necesita plugins de compilación. uv init lo escribe. |
hatchling | El proyecto es Python puro y necesita plugins de compilación o una versión tomada de git (hatch-vcs). |
flit-core | El proyecto es el paquete de Python puro más sencillo y no necesita plugins de compilación. |
setuptools | Debe seguir funcionando un setup.py existente, extensiones en C o un paso de compilación propio. |
poetry-core | El equipo gestiona el proyecto con Poetry. |
maturin | La extensión está escrita en Rust. |
scikit-build-core | La extensión es C o C++ y se compila con CMake. |
meson-python | El proyecto se compila con Meson, como hacen NumPy y SciPy. |
La figura ordena los backends desde las compilaciones que solo copian ficheros de Python hasta las que compilan código. Para obtener una versión a partir de etiquetas de Git, usa un backend que lo admita, como hatchling con hatch-vcs o setuptools con setuptools-scm.
Copia la tabla recomendada actual de la documentación del backend. No añadas wheel a los requisitos de compilación por costumbre; el backend declara lo que necesita.
[project]: los metadatos que reciben los consumidores
La tabla [project] describe la distribución: su nombre, su versión, la compatibilidad con Python, las dependencias de ejecución, los puntos de entrada y otros metadatos del índice.
Los especificadores de dependencia, como httpx>=0.27 en el ejemplo, son restricciones para un resolvedor, no una instantánea de un entorno concreto. Pasan a formar parte de los metadatos del wheel y del sdist, de modo que los instaladores posteriores pueden combinarlas con los requisitos de otros paquetes.
Los extras son una interfaz pública de instalación
[project.optional-dependencies] define extras que los consumidores pueden solicitar. Con el extra cli del ejemplo, un consumidor puede instalar weather-client[cli]. Como los nombres y los requisitos de los extras se publican, trátalos como funcionalidades del producto y nómbralos así, por ejemplo cli o postgres. No uses un extra llamado dev solo para reunir todas las herramientas de los colaboradores.
Los puntos de entrada conectan los comandos instalados con Python
La tabla [project.scripts] del ejemplo tiene una entrada, weather = "weather_client.cli:main". Una vez instalada la distribución, el entorno expone un comando weather que importa weather_client.cli y llama a main. El comando weather se instala incluso sin el extra cli, así que cli.py debe funcionar sin rich o indicar al usuario que instale weather-client[cli].
Prueba el comando desde un wheel construido, no solo desde la raíz del repositorio; el wheel es lo que reciben los usuarios. La sección de migración muestra los comandos.
[dependency-groups]: entornos locales no publicados
Los grupos de dependencias de la PEP 735 describen entornos de desarrollo o que no son paquetes, sin publicarlos como metadatos del paquete.
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
Este es el lugar adecuado para las pruebas, los linters, los generadores de documentación y otras herramientas similares de los colaboradores. También resulta útil para aplicaciones o notebooks que no construyen una distribución. include-group permite que un grupo reutilice otro; los extras no tienen nada equivalente.
La PEP 735 no define una interfaz de línea de comandos, así que cada instalador tiene sus propios flags. Los grupos y los extras se instalan así:
# Dependency groups (not published)
pip install --group test # pip 25.1 and later
uv sync --group test
uv sync --all-groups
# Extras (published)
pip install "weather-client[cli]"
uv sync --extra cli
pyproject.toml declara dependencias; no crea un entorno. Los comandos que crean uno e instalan el proyecto con un grupo dependen de la herramienta:
| Flujo | Crear el entorno | Instalar el proyecto y el grupo test |
|---|---|---|
| pip | python -m venv .venv y después activarlo | pip install -e . --group test |
| uv | uv sync crea .venv si no existe | uv sync --group test (también instala el proyecto y dev) |
[tool.*]: configuración propia de cada herramienta
Las tablas de herramientas no comparten un esquema. En el ejemplo, [tool.pytest.ini_options] y [tool.ruff] usan cada una claves que solo lee su propia herramienta. Usa una tabla [tool.*] solo si la herramienta la documenta. Estas herramientas habituales leen su configuración de pyproject.toml:
| Tarea | Herramienta | Tabla |
|---|---|---|
| Formato y linting | Ruff | [tool.ruff] |
| Pruebas | pytest | [tool.pytest.ini_options] o [tool.pytest] |
| Comprobación de tipos | mypy | [tool.mypy] |
| Cobertura | coverage.py | [tool.coverage.run], [tool.coverage.report] |
pytest 9.0 añadió [tool.pytest], que usa tipos nativos de TOML, como una lista para addopts; las versiones anteriores solo leen [tool.pytest.ini_options].
Algunos ajustes siguen perteneciendo a sus propios ficheros porque los lee otro programa o porque el fichero necesita otro formato. flake8 no lee pyproject.toml; lee .flake8, setup.cfg o tox.ini. pre-commit lee .pre-commit-config.yaml.
Las declaraciones, los locks y los ficheros de requisitos resuelven problemas distintos
La figura sigue los paquetes del fichero de ejemplo. Cada lista llega a un entorno distinto, y los usuarios reciben solo las dos listas de [project].
Cinco lugares guardan información de dependencias. Cada uno tiene una única función:
| Artefacto | Finalidad principal | Contenido habitual |
|---|---|---|
[project.dependencies] | Contrato de ejecución publicado | Requisitos directos y rangos compatibles |
[project.optional-dependencies] | Funcionalidades opcionales publicadas | Extras orientados al consumidor |
[dependency-groups] | Entornos locales no publicados | Grupos de pruebas, lint, documentación o aplicación |
| Lockfile | Reproducir un entorno resuelto | Versiones exactas, orígenes y metadatos de resolución |
requirements.txt | Entrada de instalación compatible con pip | Requisitos más opciones, restricciones, URLs o hashes específicos de pip |
Una biblioteca normalmente publica restricciones compatibles y se prueba contra un rango. Una aplicación suele incluir en el repositorio el lockfile del gestor de entornos. Las versiones exactas pertenecen al artefacto de despliegue ya resuelto, no a los metadatos públicos de una biblioteca por defecto. Por ejemplo, una biblioteca declara httpx>=0.27 y el resolvedor del usuario elige la versión. Una aplicación incluye uv.lock en el repositorio y lo instala con uv sync --locked.
Conserva un fichero de requisitos cuando una integración exija el formato o las funciones de pip. Si un proyecto gestionado con uv necesita uno, expórtalo desde el lock en lugar de mantener dos conjuntos de dependencias independientes:
uv export --format requirements.txt --output-file requirements.txt
La exportación es una salida derivada por compatibilidad. El lock sigue siendo la fuente resuelta para ese flujo de trabajo.
Una migración que conserva el comportamiento
No empieces borrando setup.py ni requirements.txt. Primero clasifica lo que hace cada fichero existente. La figura muestra adónde va cada parte de una configuración antigua en pyproject.toml y qué ficheros se quedan.
- Haz un inventario de la lógica de compilación, los metadatos, los requisitos de ejecución, los extras, los entornos de desarrollo, la configuración de herramientas, los datos del paquete y los puntos de entrada.
- Elige un backend que pueda reproducir la inclusión actual de ficheros, los artefactos compilados y las instalaciones editables.
- Traslada los metadatos publicados estáticos a
[project]; mantén explícitos los campos que de verdad son dinámicos. - Traslada los requisitos solo para colaboradores a
[dependency-groups], no a extras públicos. - Traslada la configuración de herramientas solo cuando la herramienta admita una semántica equivalente.
- Construye un sdist y un wheel, inspecciona su contenido e instala el wheel en un entorno limpio.
- Ejecuta los puntos de entrada, las pruebas, las comprobaciones de importación y la ruta de despliegue real.
- Borra la configuración antigua solo cuando los artefactos y el comportamiento coincidan.
Los pasos 6 y 7 son así para la biblioteca del ejemplo:
uv build # or: python -m build
unzip -l dist/*.whl # files in the wheel
tar -tzf dist/*.tar.gz # files in the sdist
uv venv /tmp/check
uv pip install --python /tmp/check dist/*.whl
/tmp/check/bin/weather --help
Compara ambas listas de ficheros con la compilación antigua antes de borrar nada. Un fichero de datos o un módulo que falte aparece aquí, no en la copia del repositorio.
Puede que MANIFEST.in siga siendo necesario con algunas estructuras de setuptools, y un setup.py pequeño puede seguir siendo válido para un comportamiento de compilación programático.
Un flujo de trabajo actual con uv
uv init crea una estructura distinta según el flag. Elige una de estas alternativas:
# Packaged application: src/weather_app/, uv_build, and a weather-app command
uv init weather-app
# Single main.py and no build system
uv init --no-package weather-app
# Library: src layout, uv_build, and a py.typed marker
uv init --lib weather-client
uv 0.12.0 hizo que la estructura empaquetada fuera la predeterminada. Las versiones anteriores creaban por defecto la estructura con main.py.
uv init crea los ficheros del proyecto. uv lock crea o actualiza uv.lock sin crear .venv. uv sync o uv run actualiza el lockfile y crea y sincroniza .venv cuando hace falta.
Continúa con la biblioteca creada por uv init --lib weather-client. Contiene una función hello() generada, así que añade una pequeña prueba antes de ejecutar pytest:
cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
mkdir -p tests
cat > tests/test_weather_client.py <<'PY'
from weather_client import hello
def test_hello():
assert hello() == "Hello from weather-client!"
PY
uv run --group test pytest
uv build
La prueba debería indicar 1 passed. --group test selecciona las dependencias de las pruebas incluso después de que un uv sync con las opciones predeterminadas las haya eliminado.
Inspecciona los archivos e instala el wheel en un entorno limpio antes de publicar, como muestra la sección de migración. Esta biblioteca generada no tiene un comando weather, así que comprueba import weather_client en su lugar. Cuando esté todo listo, sube a PyPI los ficheros de dist/:
uv publish
Fuera de uv, python -m build seguido de twine upload dist/* hace lo mismo.
uv coloca los requisitos de desarrollo locales en grupos de dependencias estandarizados. Su backend nativo uv_build solo admite proyectos de Python puro; las extensiones compiladas necesitan otro backend de la tabla anterior, como maturin o scikit-build-core. Consulta la documentación de configuración de proyectos de uv para ver los ajustes actuales del backend.
Varios paquetes en un repositorio
Un repositorio con varios paquetes puede convertirlos en un único workspace de uv. El pyproject.toml de la raíz enumera los miembros:
[tool.uv.workspace]
members = ["packages/*"]
Los miembros comparten un único uv.lock y un único .venv. uv run --package <name> ejecuta un comando en el contexto de un miembro.