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.

El pyproject.toml de ejemplo dividido en sus cuatro tablas: el frontend de compilación lee build-system, hatchling convierte project en metadatos del wheel que pip instala al usuario, uv sync instala el grupo test en .venv, y pytest y ruff leen sus propias tablasEl pyproject.toml de ejemplo dividido en sus cuatro tablas: el frontend de compilación lee build-system, hatchling convierte project en metadatos del wheel que pip instala al usuario, uv sync instala el grupo test en .venv, y pytest y ruff leen sus propias tablas

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_buildEl proyecto es Python puro, se gestiona con uv y no necesita plugins de compilación. uv init lo escribe.
hatchlingEl proyecto es Python puro y necesita plugins de compilación o una versión tomada de git (hatch-vcs).
flit-coreEl proyecto es el paquete de Python puro más sencillo y no necesita plugins de compilación.
setuptoolsDebe seguir funcionando un setup.py existente, extensiones en C o un paso de compilación propio.
poetry-coreEl equipo gestiona el proyecto con Poetry.
maturinLa extensión está escrita en Rust.
scikit-build-coreLa extensión es C o C++ y se compila con CMake.
meson-pythonEl 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.

Backends de compilación en cuatro columnas, desde Python puro sin plugins (uv_build, flit-core) hasta Python puro con plugins (hatchling), una compilación antigua con setup.py (setuptools) y extensiones compiladas (maturin, scikit-build-core, meson-python)Backends de compilación en cuatro columnas, desde Python puro sin plugins (uv_build, flit-core) hasta Python puro con plugins (hatchling), una compilación antigua con setup.py (setuptools) y extensiones compiladas (maturin, scikit-build-core, meson-python)

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:

FlujoCrear el entornoInstalar el proyecto y el grupo test
pippython -m venv .venv y después activarlopip install -e . --group test
uvuv sync crea .venv si no existeuv 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:

TareaHerramientaTabla
Formato y lintingRuff[tool.ruff]
Pruebaspytest[tool.pytest.ini_options] o [tool.pytest]
Comprobación de tiposmypy[tool.mypy]
Coberturacoverage.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].

Una cuadrícula de las cuatro listas de dependencias frente a tres entornos: hatchling solo en el entorno temporal de compilación, httpx para usuarios y colaboradores, rich solo para usuarios con el extra cli, y pytest y pytest-cov solo en el .venv del colaboradorUna cuadrícula de las cuatro listas de dependencias frente a tres entornos: hatchling solo en el entorno temporal de compilación, httpx para usuarios y colaboradores, rich solo para usuarios con el extra cli, y pytest y pytest-cov solo en el .venv del colaborador

Cinco lugares guardan información de dependencias. Cada uno tiene una única función:

ArtefactoFinalidad principalContenido habitual
[project.dependencies]Contrato de ejecución publicadoRequisitos directos y rangos compatibles
[project.optional-dependencies]Funcionalidades opcionales publicadasExtras orientados al consumidor
[dependency-groups]Entornos locales no publicadosGrupos de pruebas, lint, documentación o aplicación
LockfileReproducir un entorno resueltoVersiones exactas, orígenes y metadatos de resolución
requirements.txtEntrada de instalación compatible con pipRequisitos 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.

Ficheros antiguos asignados a su nuevo destino: los metadatos de setup.py, install_requires, extras_require y los puntos de entrada pasan a tablas de project, los requisitos de desarrollo a dependency-groups, pytest.ini a una tabla tool; un setup.py pequeño, MANIFEST.in, .flake8 y un requirements.txt exportado desde uv.lock se quedan fueraFicheros antiguos asignados a su nuevo destino: los metadatos de setup.py, install_requires, extras_require y los puntos de entrada pasan a tablas de project, los requisitos de desarrollo a dependency-groups, pytest.ini a una tabla tool; un setup.py pequeño, MANIFEST.in, .flake8 y un requirements.txt exportado desde uv.lock se quedan fuera

  1. 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.
  2. Elige un backend que pueda reproducir la inclusión actual de ficheros, los artefactos compilados y las instalaciones editables.
  3. Traslada los metadatos publicados estáticos a [project]; mantén explícitos los campos que de verdad son dinámicos.
  4. Traslada los requisitos solo para colaboradores a [dependency-groups], no a extras públicos.
  5. Traslada la configuración de herramientas solo cuando la herramienta admita una semántica equivalente.
  6. Construye un sdist y un wheel, inspecciona su contenido e instala el wheel en un entorno limpio.
  7. Ejecuta los puntos de entrada, las pruebas, las comprobaciones de importación y la ruta de despliegue real.
  8. 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.

Referencias