pyproject.toml, el manifiesto moderno de proyectos Python

Intermedio
Python
Python
Actualizado: 05/05/2026

Qué es pyproject.toml y por qué existe

flowchart TD
    PYPROJ["pyproject.toml (TOML)"] --> BUILD["[build-system] PEP 518"]
    PYPROJ --> PROJ["[project] PEP 621: metadatos"]
    PYPROJ --> TOOL["[tool.*] configuración herramientas"]
    PROJ --> NAME["name, version, requires-python"]
    PROJ --> DEPS["dependencies, optional-dependencies"]
    TOOL --> RUFF["[tool.ruff]"]
    TOOL --> PYTEST["[tool.pytest]"]
    TOOL --> MYPY["[tool.mypy]"]
    PYPROJ -.->|reemplaza| LEGACY["setup.py + setup.cfg + requirements.txt"]

Antes de los PEPs 518 y 621, la configuración de un proyecto Python estaba dispersa entre setup.py (metadatos y dependencias ejecutables como código Python), setup.cfg (metadatos declarativos), requirements.txt (dependencias sueltas sin relación formal con el paquete) y varios ficheros de configuración de herramientas (.flake8, tox.ini, pytest.ini). Era frágil, duplicaba información y obligaba a instalar el propio setup.py para poder leer las dependencias.

pyproject.toml unifica todo en un único archivo con formato TOML, diseñado específicamente para ficheros de configuración legibles por humanos y máquinas. Define dos bloques estándar:

  • PEP 518 ([build-system]): específica qué herramienta usa el proyecto para construirse (setuptools, hatch, flit, poetry, etcétera).
  • PEP 621 ([project]): declara metadatos del paquete (nombre, versión, dependencias, entry points) de forma declarativa.

A esto se suman secciones [tool.XYZ] donde cada herramienta guarda su configuración.

Hoy, en 2026, cualquier proyecto Python moderno (ya sea una librería, una aplicación web, un CLI o un paquete científico) usa pyproject.toml como único manifiesto. Las opciones clásicas con setup.py se consideran legacy.

Estructura mínima de un pyproject.toml

Un proyecto básico declara estos tres bloques:

[project]
name = "mi-app"
version = "0.1.0"
description = "Aplicación de ejemplo"
requires-python = ">=3.13"
dependencies = [
    "requests>=2.32",
    "click>=8.1",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-cov>=5.0",
    "ruff>=0.8",
]

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

Con esto, cualquier herramienta compatible con PEP 621 (pip, uv, poetry, hatch, ...) puede instalar, construir y publicar el paquete.

Metadatos del proyecto

La sección [project] describe el paquete. Campos habituales:

[project]
name = "catalogo-productos"
version = "1.2.0"
description = "Catálogo de productos con API REST"
readme = "README.md"
requires-python = ">=3.13"
license = { text = "MIT" }
authors = [
    { name = "Ana Gutierrez", email = "ana@example.com" },
]
keywords = ["productos", "catalogo", "api"]
classifiers = [
    "Programming Language :: Python :: 3.13",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

Los classifiers son etiquetas estándar reconocidas por PyPI que ayudan a categorizar el paquete en su índice. La lista completa está en la documentación oficial de PyPI.

Dependencias

Hay tres tipos principales de dependencias:

Dependencias de ejecución (necesarias para que el paquete funcione en producción):

[project]
dependencies = [
    "fastapi>=0.115",
    "pydantic>=2.9",
    "sqlalchemy>=2.0",
]

Las versiones siguen PEP 440: ==, >=, <, !=, ~= (compatible release), y permite rangos como >=2.0,<3.0.

Dependencias opcionales (se instalan solo cuando el usuario las pide explícitamente):

[project.optional-dependencies]
postgres = ["psycopg[binary]>=3.2"]
mysql = ["pymysql>=1.1"]
redis = ["redis>=5.0"]

Se instalan con pip install mi-paquete[postgres] o uv sync --extra postgres.

Grupos de dependencias (PEP 735, específicos para herramientas y tests, no parte del paquete distribuido):

[dependency-groups]
dev = [
    "pytest>=8.0",
    "pytest-cov>=5.0",
    "ruff>=0.8",
    "mypy>=1.11",
]
docs = [
    "mkdocs>=1.6",
    "mkdocs-material>=9.5",
]

Con uv sync --group dev se instalan solo las de desarrollo.

Scripts y entry points

Para exponer un comando de línea (como mi-app --help), se declara un script que apunta a una función:

[project.scripts]
mi-app = "mi_app.cli:main"
mi-app-admin = "mi_app.admin:run"

Cuando alguien instala tu paquete, pip (o uv) crea automáticamente un ejecutable con ese nombre que invoca a la función especificada.

Para entry points en general (plugins, drivers):

[project.entry-points."mi_app.plugins"]
exportador_json = "mi_app.plugins.json_exp:Exportador"
exportador_csv = "mi_app.plugins.csv_exp:Exportador"

Esto permite que otro paquete extienda el tuyo cuando ambos estén instalados.

URLs del proyecto

[project.urls]
Homepage = "https://ejemplo.com/mi-app"
Documentation = "https://mi-app.readthedocs.io"
Repository = "https://github.com/ejemplo/mi-app"
Issues = "https://github.com/ejemplo/mi-app/issues"
Changelog = "https://github.com/ejemplo/mi-app/blob/main/CHANGELOG.md"

Estas URLs aparecen en la página del paquete en PyPI.

Build system

La sección [build-system] específica qué herramienta construye el paquete (de forma estándar en PEP 517):

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

Backends populares en 2026:

  • hatchling: parte del proyecto Hatch, rápido y minimalista, muy usado.
  • setuptools: histórico, estable, todavía el más usado por volumen.
  • flit-core: minimalista para librerías puras Python.
  • poetry-core: si el proyecto usa Poetry.
  • uv: uv propio puede actuar como backend en proyectos uv.

El usuario final no necesita instalar el backend manualmente; pip o uv se encargan al construir.

Configuración de herramientas

Cualquier herramienta compatible puede leer su configuración desde [tool.xxx] en el mismo pyproject.toml. Esto centraliza todo:

[tool.ruff]
line-length = 100
target-version = "py313"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra --strict-markers --cov=src"

[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*"]

[tool.mypy]
python_version = "3.13"
strict = true
ignore_missing_imports = true

Con esto, todo el equipo sabe que la configuración del proyecto está en un solo fichero. Los archivos clásicos .flake8, setup.cfg, tox.ini y pytest.ini sobran.

Layout de proyecto recomendado: src

La convención moderna es usar el src layout para evitar que los imports relativos del entorno de desarrollo falseen los resultados de los tests:

mi-app/
  pyproject.toml
  README.md
  src/
    mi_app/
      __init__.py
      cli.py
      domain/
        __init__.py
        models.py
  tests/
    test_cli.py
    test_models.py

En pyproject.toml, se indica el layout:

[tool.hatch.build.targets.wheel]
packages = ["src/mi_app"]

Con este layout, los tests no pueden importar por accidente desde el directorio local sin pasar por la instalación del paquete, que es exactamente lo que ocurre cuando alguien lo usa en producción.

Construir y publicar

Con un pyproject.toml bien formado, publicar el paquete es un proceso estándar:

uv build                              # genera sdist y wheel en dist/
uv publish                            # sube a PyPI

Con pip y twine clásicos:

python -m build
python -m twine upload dist/*

No se necesita setup.py ni setup.cfg.

Evolución y estándares

El ecosistema Python evoluciona rápido en este área. Los PEPs relevantes conviene conocerlos:

  • PEP 517: interfaz estándar de build backends.
  • PEP 518: declaración [build-system] en pyproject.toml.
  • PEP 621: metadatos del proyecto en [project].
  • PEP 660: instalaciones editables modernas (pip install -e .).
  • PEP 735: grupos de dependencias [dependency-groups].
  • PEP 723: metadatos embebidos en scripts sueltos.

Seguir estos estándares garantiza que tu proyecto sea compatible con cualquier herramienta moderna (pip, uv, poetry, hatch, pdm, rye) sin cambios en la configuración, porque todas leen y escriben el mismo formato declarativo.

Adoptar pyproject.toml como fuente única de verdad es la práctica recomendada para cualquier proyecto Python profesional iniciado a partir de 2022, y a día de hoy el consenso es total.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Python

Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender el papel de pyproject.toml como único manifiesto de proyectos Python modernos. Declarar metadatos y dependencias con PEP 621. Configurar grupos de dependencias opcionales y de desarrollo. Centralizar la configuración de herramientas como ruff, pytest y mypy.