Saltar a contenido

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.

Estructura

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.

Ecosistema de herramientas

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 comando awesome-cli que ejecuta la función main en awesome_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:

Flujo de migración

  1. Añade [build-system]. Si no tienes claro qué backend usar, empieza con requires = ["setuptools>=61", "wheel"].
  2. Mueve los metadatos a [project]. Transfiere nombre, versión y dependencias desde setup.py o setup.cfg.
  3. Convierte las dependencias de desarrollo. Ponlas en [project.optional-dependencies].dev.
  4. Configura las herramientas. Añade secciones [tool.*] para Black, pytest, mypy, etc.
  5. 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

  1. No fijes versiones exactas en librerías. Usa rangos como requests>=2.30 para que tu librería no entre en conflicto con lo que ya haya instalado en el entorno de alguien.
  2. Sí fija versiones en aplicaciones. Usa un lockfile (uv.lock, poetry.lock) para builds reproducibles.
  3. Agrupa las dependencias de desarrollo. Mantén las dependencias de testing, linting y documentación en grupos opcionales separados (dev, test, docs).
  4. 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.