Traducción automática
Este artículo se tradujo automáticamente a partir de la versión original en inglés.
Guía de pyproject.toml: empaquetado de Python, dependencias y configuración de herramientas
Si alguna vez has abierto un proyecto Python y has intentado averiguar dónde viven realmente las dependencias, la configuración de build y la configuración de herramientas, ya conoces el dolor. setup.py, setup.cfg, requirements.txt, MANIFEST.in, más un puñado de dotfiles para cada linter y formateador: cada uno leyendo de sitios distintos.
pyproject.toml concentra la mayor parte de eso en un solo archivo.
TL;DR
pyproject.toml es, más o menos, el package.json de Python. Un único archivo contiene los metadatos del proyecto, las dependencias y la configuración de herramientas. Tanto si usas .venv, pyenv o uv, ponerlo todo aquí facilita la puesta en marcha y la colaboración.
¿Qué es pyproject.toml?
Es un archivo de configuración que vive en la raíz de tu proyecto Python, escrito en TOML (piensa en archivos INI, pero con una especificación real). Dos PEP definieron lo que hace hoy:
- PEP 518 (2016) introdujo la tabla
[build-system]para que las herramientas de build pudieran declarar sus requisitos de forma estándar. - PEP 621 (2020) añadió la tabla
[project]para los metadatos básicos del paquete: nombre, versión, dependencias y ese tipo de cosas.
Hoy, la mayoría de herramientas del ecosistema Python (Black, isort, pytest, Ruff, mypy) leen su configuración desde secciones [tool.*] de este archivo.
Por qué importa
Menos archivos que perseguir
Un proyecto legacy típico se manejaba con setup.py, setup.cfg, requirements.txt, MANIFEST.in y una bandada de dotfiles (.flake8, .coveragerc y similares). La mayoría de eso se concentra en un solo sitio.
Builds agnósticos al backend
Cuando ejecutas pip install ., pip lee pyproject.toml e instala las herramientas de build que necesite tu proyecto: setuptools, flit, hatchling, lo que prefieras. Ya no estás atado a setuptools.
Un único lugar para la configuración de herramientas
Linters, formateadores, runners de tests y comprobadores de tipos saben que deben mirar aquí. Tu IDE, tu pipeline de CI y tus compañeros leen del mismo archivo.
Anatomía de un pyproject.toml
Un archivo típico tiene tres secciones principales:
# 1. Build system - tells pip/build how to package your project
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# 2. Project metadata and dependencies
[project]
name = "awesome-app"
version = "0.1.0"
description = "Short demo of pyproject.toml"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.111",
"uvicorn[standard]>=0.30",
]
# Expose CLI commands
[project.scripts]
awesome-cli = "awesome_app.cli:main"
# Optional dependencies (e.g., for development)
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
# 3. Tool configuration
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.pytest.ini_options]
addopts = "-ra -q"
testpaths = ["tests"]
Desglose
[build-system]
Obligatorio si quieres empaquetar o distribuir tu proyecto. Indica a pip y a las herramientas de build (como python -m build) qué backend usar.
[project]
Los metadatos de tu paquete. Aquí es donde viven las dependencias en lugar de requirements.txt.
dependencies: los requisitos en tiempo de ejecución de tu paquete.optional-dependencies: grupos de dependencias extra (dev,test,docs).scripts: crea comandos ejecutables. En el ejemplo anterior, instalar el paquete te da un comandoawesome-clique ejecuta la funciónmainenawesome_app/cli.py.
[tool.*]
Configuración para cualquier herramienta que lo soporte. Cada herramienta tiene su propio namespace: [tool.pytest.ini_options], [tool.mypy], [tool.ruff] y el resto.
¿Sustituye a requirements.txt?
Para flujos de trabajo modernos, sí. Herramientas como Poetry, PDM, Hatch y uv almacenan las dependencias directamente en la sección [project] y generan lockfiles para asegurar la reproducibilidad.
Probablemente aún quieras requirements.txt si:
- Estás trabajando con un sistema de despliegue legacy que lo espera.
- Tienes un script de CI que aún no se ha actualizado.
La mayoría de herramientas modernas pueden exportarlo desde tu pyproject.toml cuando haga falta:
uv export > requirements.txt
Elegir un backend de build
El backend de build es una de las decisiones que más confusión generan. Aquí tienes una comparación rápida:
| Backend | Mejor para | Ventajas | Inconvenientes |
|---|---|---|---|
| Hatchling | Estándar moderno | Rápido, extensible, soporta plugins | Más nuevo, menos soporte legacy |
| Flit | Paquetes simples | Extremadamente simple, configuración cero | No sirve para builds complejos (extensiones C) |
| Setuptools | Legacy, complejo | Soporta de todo (extensiones C, etc.) | Más lento, más configuración |
| Poetry | Usuarios de Poetry | Integrado con el ecosistema de Poetry | Te ata al flujo de trabajo de Poetry |
Para proyectos nuevos de Python puro, Hatchling es una opción razonable por defecto. Es lo que uv init escribe por ti.
Migrar un proyecto existente
Si tienes un proyecto Python legacy, así es como puedes modernizarlo:
- Añade
[build-system]. Si no tienes claro qué backend usar, empieza conrequires = ["setuptools>=61", "wheel"]. - Mueve los metadatos a
[project]. Transfiere nombre, versión y dependencias desdesetup.pyosetup.cfg. - Convierte las dependencias de desarrollo. Ponlas en
[project.optional-dependencies].dev. - Configura las herramientas. Añade secciones
[tool.*]para Black, pytest, mypy, etc. - Decide qué hacer con
requirements.txt. O bien elimínalo, o bien genéralo desde tu lockfile para sistemas legacy que lo necesiten.
Después de la migración, normalmente puedes borrar setup.py, setup.cfg y la mayoría de esos dotfiles de configuración.
Algunas funciones útiles
Entry points de CLI
En lugar del antiguo bloque console_scripts en setup.py, usa [project.scripts]:
[project.scripts]
my-tool = "my_package.main:run"
Cuando alguien instala tu paquete, puede escribir my-tool en su terminal.
Workspaces (monorepos)
Herramientas como uv y hatch soportan workspaces, que te permiten gestionar varios paquetes en un único repo:
[tool.uv.workspace]
members = ["packages/*"]
Puedes desarrollar varios paquetes interdependientes e instalarlos todos en un mismo entorno virtual para hacer pruebas.
Flujos de trabajo típicos con uv
Empezar un proyecto nuevo
uv init my_app # creates folder with pyproject.toml and .venv
cd my_app
uv add requests fastapi # adds to [project.dependencies] and installs
uv run pytest # runs tests in the venv
Ejecutar scripts
Puedes definir scripts en pyproject.toml (usando un task runner como poe o hatch) o simplemente llamar a uv run:
uv run python main.py
Consejos prácticos
- No fijes versiones exactas en librerías. Usa rangos como
requests>=2.30para que tu librería no entre en conflicto con lo que ya haya instalado en el entorno de alguien. - Sí fija versiones en aplicaciones. Usa un lockfile (
uv.lock,poetry.lock) para builds reproducibles. - Agrupa las dependencias de desarrollo. Mantén las dependencias de testing, linting y documentación en grupos opcionales separados (
dev,test,docs). - No metas todas las opciones en
pyproject.toml. Limítate a valores por defecto a nivel de proyecto y deja que los archivos de configuración específicos de cada herramienta se encarguen del resto si la cosa se complica.
La verdadera ventaja de pyproject.toml es, sobre todo, eliminar pequeñas fricciones del día a día. Dejas de buscar qué archivo controla el build. La configuración del linter está al lado de las dependencias. Pip y tu IDE por fin se ponen de acuerdo sobre qué está instalado. Elige un backend de build, pon tus dependencias en [project] y deja que tus herramientas lean de un único sitio. Si empiezas desde cero, uv init te lleva gran parte del camino con un solo comando.