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 antiguos solían repartir la configuración entre setup.py, setup.cfg, los archivos de requirements, MANIFEST.in y archivos independientes para las herramientas de desarrollo. pyproject.toml ofrece un lugar común para estas cuestiones, aunque no sustituye todos los archivos del proyecto ni convierte cada sección en parte de un único estándar.

Esta guía recorre el archivo desde la configuración del build hasta los metadatos del proyecto y la configuración de las herramientas. Cuatro responsables mantienen diferenciadas las secciones: qué construye el proyecto, qué publica el proyecto, qué necesitan localmente los colaboradores y qué configura cada herramienta.

En resumen. Usa [build-system] para el backend que construye una distribución, [project] para los metadatos publicados y los requisitos de runtime, [dependency-groups] para los entornos de desarrollo no publicados y [tool.*] únicamente cuando la documentación de esa herramienta lo indique. Una declaración de dependencias no es un lockfile.

Un archivo, cuatro responsables

Los cuatro límites de responsabilidad en pyproject.tomlLos cuatro límites de responsabilidad en pyproject.toml

Tres estándares de empaquetado establecieron la estructura principal:

  • PEP 518 define los requisitos del build-system.
  • 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 namespace bajo [tool]. No hay nada que estandarice su contenido: cada herramienta define sus propias claves y comportamiento.

Este es un ejemplo de una pequeña librerí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"

Cada lista de dependencias responde a una pregunta diferente.

[build-system]: cómo se convierte el código fuente en una distribución

Un build frontend como python -m build, pip o uv invoca un build backend. El backend decide cómo se convierte el árbol de código fuente en un sdist o un wheel, y qué archivos se incluyen en esos artefactos.

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

requires contiene las dependencias necesarias para ejecutar el backend en su entorno de build aislado. No es la lista de dependencias de runtime de tu paquete.

Declara un build system cuando el proyecto genere una distribución o necesite instalar su propio código como paquete. Un proyecto que no sea un paquete también puede usar pyproject.toml; PEP 735 incluso permite un archivo que contenga únicamente grupos de dependencias. Los gestores de entornos difieren en cómo tratan un proyecto sin build system, así que toma esta decisión de forma deliberada.

Elige el backend a partir de los requisitos del build:

  • layout de Python puro y necesidades de selección de archivos
  • extensiones compiladas o sistemas de build externos
  • requisitos de versiones dinámicas o archivos generados
  • comportamiento de las instalaciones editables
  • madurez del backend en el pipeline de release

Copia de la documentación del backend su tabla recomendada actual. No añadas wheel a los requisitos del build por costumbre; el backend declara qué necesita.

[project]: los metadatos que reciben los consumidores

La tabla [project] describe la distribución: su nombre, versión, compatibilidad con Python, dependencias de runtime, puntos de entrada y otros metadatos del índice.

[project]
name = "weather-client"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27"]

Los specifiers de dependencias son restricciones para un resolver, 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 puedan combinarlos 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:

[project.optional-dependencies]
cli = ["rich>=13"]
postgres = ["psycopg[binary]>=3.2"]

Un consumidor puede instalar weather-client[cli]. Como los nombres de los extras y sus requisitos se publican, trátalos como capacidades del producto. No uses un extra llamado dev simplemente para agrupar todas las herramientas de los colaboradores.

Los entry points conectan los comandos instalados con Python

[project.scripts]
weather = "weather_client.cli:main"

Una vez instalada la distribución, el entorno expone weather, que importa y llama a weather_client.cli:main. Prueba el comando desde un wheel construido, no solo desde la raíz del repositorio; el wheel es lo que reciben los usuarios.

[dependency-groups]: entornos locales no publicados

Los grupos de dependencias de 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 límite adecuado para tests, linters, generadores de documentación y herramientas similares de los colaboradores. También resulta útil para aplicaciones o notebooks que no construyen una distribución.

Los grupos de dependencias son datos estandarizados, pero las interfaces de los instaladores siguen variando. Comprueba cómo instala, bloquea y resuelve esos grupos el gestor de entornos elegido. PEP 735 no define una interfaz de línea de comandos universal.

[tool.*]: configuración propiedad de una herramienta

Las tablas de las herramientas no comparten un esquema:

[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]

[tool.ruff]
line-length = 100

Usa una tabla [tool.*] únicamente si la herramienta la documenta. Algunas opciones siguen perteneciendo a sus propios archivos porque las consume otro ecosistema, porque el archivo necesita un formato diferente o porque la configuración resulta más clara por separado. pyproject.toml es un punto de coordinación. No obliga a centralizar todos los ajustes.

Consumidores de configuración alrededor de pyproject.tomlConsumidores de configuración alrededor de pyproject.toml

Las declaraciones, los locks y los archivos de requirements resuelven problemas distintos

Una fuente habitual de confusión consiste en tratar cada artefacto de dependencias como una fuente de verdad alternativa.

ArtefactoPropósito principalContenido habitual
[project.dependencies]Contrato de runtime publicadoRequisitos directos y rangos compatibles
[project.optional-dependencies]Capacidades publicadas bajo demandaExtras orientados a los consumidores
[dependency-groups]Entornos locales no publicadosGrupos de tests, lint, documentación o aplicaciones
LockfileReproducir un entorno resueltoVersiones exactas, fuentes y metadatos de resolución
requirements.txtEntrada de instalación compatible con pipRequisitos y opciones específicas de pip, constraints, URL o hashes

Una librería normalmente publica restricciones compatibles y realiza tests contra un rango. Una aplicación suele incluir en el control de versiones el lockfile del gestor de entornos. Los pins exactos pertenecen al artefacto de despliegue resuelto, no a los metadatos públicos de una librería de forma indiscriminada.

Conserva un archivo de requirements cuando una integración requiera el formato o las funcionalidades 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 de compatibilidad derivada. El lock sigue siendo la fuente resuelta para ese flujo de trabajo.

Una migración que preserve el comportamiento

Una migración a pyproject.toml basada en la verificaciónUna migración a pyproject.toml basada en la verificación

No empieces eliminando setup.py ni requirements.txt. Primero clasifica qué hace cada archivo existente.

  1. Haz inventario de la lógica de build, los metadatos, los requisitos de runtime, los extras, los entornos de desarrollo, la configuración de herramientas, los datos del paquete y los entry points.
  2. Selecciona un backend que pueda reproducir la inclusión actual de archivos, los artefactos compilados y las instalaciones editables.
  3. Mueve los metadatos estáticos publicados a [project]; mantén explícitos los campos que sean realmente dinámicos.
  4. Mueve los requisitos exclusivos de los colaboradores a [dependency-groups], no a extras públicos.
  5. Mueve la configuración de las herramientas únicamente 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 entry points, los tests, las comprobaciones de importación y el flujo de despliegue real.
  8. Elimina la configuración antigua solo después de que los artefactos y el comportamiento coincidan.

MANIFEST.in puede seguir siendo necesario con algunos layouts de setuptools, y un pequeño setup.py puede seguir siendo válido para el comportamiento programático del build. La modernización traslada la responsabilidad entre archivos. El objetivo no es eliminar archivos.

Un flujo de trabajo actual con uv

uv distingue entre aplicaciones y librerías al crear un proyecto:

# Non-library application template
uv init weather-app

# Packaged library with a src layout and build system
uv init --lib weather-client

uv init crea los archivos del proyecto. La primera operación del proyecto, como uv run, uv sync o uv lock, crea el lockfile y el .venv persistente cuando es necesario.

cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
uv run pytest
uv build

Actualmente, uv coloca los requisitos de desarrollo local en grupos de dependencias estandarizados. Su backend nativo uv_build es una opción para proyectos de Python puro; las extensiones compiladas necesitan una alternativa adecuada, como maturin o scikit-build-core. Consulta la documentación de configuración de proyectos de uv para conocer los ajustes actuales del backend.

Conclusión

pyproject.toml resulta claro cuando cada tabla tiene un único público. El aislamiento del build pertenece a [build-system] y el comportamiento publicado a [project]. Los entornos de los colaboradores pertenecen a [dependency-groups]. Las opciones de las herramientas pertenecen a la herramienta que las define.

Cuando estos límites están bien establecidos, el archivo resulta más fácil de revisar y las migraciones dejan de confundir los metadatos del paquete con el entorno resuelto de una máquina concreta.

Referencias