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.
Recipe
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:
- Starting any new Python library or application
- Consolidating scattered
setup.cfg,setup.py, andruff.tomlfiles - Configuring build backends, linters, and test runners in one place
Working Example
[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:
- PEP 621
[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
Deep Dive
How It Works
- PEP 518 introduced
[build-system]- every installer reads this first - PEP 621 standardized
[project]metadata (replacessetup.pykwargs) - Tools read their own
[tool.<name>]sections - no central registry needed - One file means one source of truth for humans and CI
Key Sections
| 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 |
Python Notes
# Dynamic version from VCS (hatchling)
[project]
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"# Monorepo workspace (uv)
[tool.uv.workspace]
members = ["packages/*"]Gotchas
- Missing
[build-system]-pip install .fails with opaque errors. Fix: always includerequiresandbuild-backend. - Duplicate config in
setup.pyandpyproject.toml- tools read different files. Fix: migrate fully topyproject.toml; delete legacy files. - Wrong package discovery - empty wheel with no modules. Fix: configure
packagesin[tool.hatch.build.targets.wheel]or usesrc/layout. requires-pythontoo narrow or too wide - installs fail or run on unsupported versions. Fix: match your CI matrix lower bound.- Secrets in
[tool.*]- tokens committed to git. Fix: use env vars; keep only non-secret defaults in TOML.
Alternatives
| 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) |
FAQs
Do I still need setup.py?
No for most projects. pyproject.toml with a build backend replaces it entirely.
Where do dev dependencies go?
Use [dependency-groups] (PEP 735) or [project.optional-dependencies] for optional groups.
Can I configure multiple tools in one file?
Yes. Each tool reads its own [tool.<name>] section. ruff, pytest, mypy, and hatch all coexist.
What build backend should I choose?
hatchling for most packages. Use setuptools for legacy extensions. Use maturin for Rust extensions.
How do entry points work?
[project.scripts] maps CLI names to module:function callables. Installed into the venv's bin/ on pip install.
Is pyproject.toml required for applications?
Not strictly, but it is the standard for dependency declaration, tooling config, and reproducibility.
How do I specify Python 3.14 only?
requires-python = ">=3.14,<3.15" or ==3.14.* depending on how strictly you want to pin.
Can tools conflict in the same file?
No. Each [tool.*] section is independent. Only [project] metadata is shared.
How do I validate pyproject.toml syntax?
pip install validate-pyproject or let your package manager (uv sync) report parse errors.
Should I commit pyproject.toml?
Always. It is the project manifest. Lockfiles (uv.lock, poetry.lock) should also be committed.
Related
- uv - The Fast Toolchain - day-to-day workflow
- Dependency Resolution & Lockfiles - version pinning
- Editable Installs & Local Packages - src layout
- Build Backends - hatchling vs setuptools
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+.