pyproject.toml Explained
pyproject.toml is the single configuration file for modern Python projects. It holds package metadata, dependencies, build settings, and tool configuration (ruff, pytest, mypy) in one TOML file.
Search across all documentation pages
pyproject.toml is the single configuration file for modern Python projects. It holds package metadata, dependencies, build settings, and tool configuration (ruff, pytest, mypy) in one TOML file.
Quick-reference recipe card - copy-paste ready.
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = ["fastapi>=0.115"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 88
target-version = "py314"When to reach for this:
setup.cfg, setup.py, and ruff.toml files[project]
name = "invoice-api"
version = "0.1.0"
description = "Invoice management API"
readme = "README.md"
requires-python = ">=3.14"
license = "MIT"
authors = [{ name = "Ada Lovelace", email = "ada@example.com" }]
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.34",
"pydantic>=2",
"sqlalchemy>=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.9", "mypy>=1.14"]
[project.scripts]
invoice-api = "invoice_api.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/invoice_api"]
[tool.ruff]
line-length = 88
target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
[tool.pytest.ini_options]
testpaths = ["tests"]uv sync --group dev
uv run pytest
uv run ruff check .What this demonstrates:
[project] block with metadata, deps, and entry points[build-system] tells installers which backend builds wheels[tool.*] sections configure ruff and pytest without separate config files[project.scripts] creates a CLI command on install[build-system] - every installer reads this first[project] metadata (replaces setup.py kwargs)[tool.<name>] sections - no central registry needed| Section | Purpose |
|---|---|
[project] | Name, version, deps, scripts, classifiers |
[build-system] | Build backend (hatchling, setuptools, poetry-core) |
[dependency-groups] | Dev/docs/test groups (PEP 735) |
[tool.ruff] | Linter and formatter config |
[tool.pytest.ini_options] | Test discovery and markers |
[tool.mypy] | Type checker settings |
[tool.uv] | uv-specific index and workspace config |
# Dynamic version from VCS (hatchling)
[project]
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"# Monorepo workspace (uv)
[tool.uv.workspace]
members = ["packages/*"][build-system] - pip install . fails with opaque errors. Fix: always include requires and build-backend.setup.py and pyproject.toml - tools read different files. Fix: migrate fully to pyproject.toml; delete legacy files.packages in [tool.hatch.build.targets.wheel] or use src/ layout.requires-python too narrow or too wide - installs fail or run on unsupported versions. Fix: match your CI matrix lower bound.[tool.*] - tokens committed to git. Fix: use env vars; keep only non-secret defaults in TOML.
| Alternative | Use When | Don't Use When |
|---|---|---|
setup.py only | Untouched legacy project | New projects (deprecated pattern) |
setup.cfg | Very old setuptools projects | You need tool config beyond metadata |
| Split config files | Tool lacks pyproject.toml support | The tool supports [tool.*] (prefer one file) |
No for most projects. pyproject.toml with a build backend replaces it entirely.
Use [dependency-groups] (PEP 735) or [project.optional-dependencies] for optional groups.
Yes. Each tool reads its own [tool.<name>] section. ruff, pytest, mypy, and hatch all coexist.
hatchling for most packages. Use setuptools for legacy extensions. Use maturin for Rust extensions.
[project.scripts] maps CLI names to module:function callables. Installed into the venv's bin/ on pip install.
Not strictly, but it is the standard for dependency declaration, tooling config, and reproducibility.
requires-python = ">=3.14,<3.15" or ==3.14.* depending on how strictly you want to pin.
No. Each [tool.*] section is independent. Only [project] metadata is shared.
pip install validate-pyproject or let your package manager (uv sync) report parse errors.
Always. It is the project manifest. Lockfiles (uv.lock, poetry.lock) should also be committed.
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026