pyproject.toml: The Standard Python Project Configuration File
pyproject.toml is the standard configuration file for a Python project. It is written in TOML and lives in the project root. A single file captures three kinds of information that older Python projects scattered across setup.py, setup.cfg, MANIFEST.in, requirements.txt, .flake8, mypy.ini, pytest.ini, and tox.ini: project metadata, build-system requirements, and third-party tool configuration.
PEP 518 introduced pyproject.toml in 2016 to declare build-system requirements in a tool-agnostic way. PEP 621 extended it in 2020 with a standardized [project] table for project metadata. Together, the two PEPs make pyproject.toml the canonical home for a Python project’s configuration.
Tip
Run uv init to scaffold a new project with a pyproject.toml pre-configured with modern defaults. See How to create your first Python project for a walkthrough.
Structure
A pyproject.toml file contains three standard top-level tables. All are optional, but most projects use at least one.
| Table | Purpose | Defined by |
|---|---|---|
[build-system] |
Which build backend to use and its dependencies | PEP 518 |
[project] |
Project metadata: name, version, dependencies, authors | PEP 621 |
[tool.<name>] |
Configuration for third-party tools (ruff, mypy, pytest, uv, etc.) | PEP 518 (reserved namespace) |
A minimal pyproject.toml for a library looks like this:
[project]
name = "mypackage"
version = "0.1.0"
description = "A small example package."
requires-python = ">=3.9"
dependencies = [
"requests>=2.28",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"The [project] Table
The [project] table holds standard project metadata defined by PEP 621. Any compliant build backend reads these fields; they do not depend on which tool is in use.
[project]
name = "mypackage"
version = "1.2.3"
description = "A short summary of the project."
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Jane Doe", email = "[email protected]" },
]
keywords = ["example", "packaging"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
]
dependencies = [
"requests>=2.28",
"pandas~=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff"]
docs = ["sphinx", "furo"]
[project.urls]
Homepage = "https://example.com/mypackage"
Repository = "https://github.com/example/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Issues = "https://github.com/example/mypackage/issues"
[project.scripts]
mypackage = "mypackage.cli:main"Required and recommended fields
name(required): the distribution name used on PyPI. Lowercased and normalized per PEP 503 for lookups.version(required unless listed indynamic): a PEP 440-compliant version string, ordynamic = ["version"]to defer to the build backend.description: a one-line summary shown on PyPI.readme: a path to the long-form description file. A string is interpreted as a filename; a table supports explicit content types and inline text.requires-python: a version specifier (for example,">=3.9") that declares which Python interpreters the project supports. Installers refuse to install on incompatible interpreters.license: an SPDX identifier (for example,"MIT","Apache-2.0") as standardized by PEP 639. Older pyproject.toml files uselicense = { file = "LICENSE" }orlicense = { text = "MIT" }.authorsandmaintainers: lists of{ name, email }tables.dependencies: runtime dependencies as PEP 508 requirement strings. Each entry accepts version specifiers, extras, and environment markers.optional-dependencies: a table of named extras. Installingmypackage[dev]pulls in thedevlist.keywordsandclassifiers: PyPI search metadata.urls: a table of labeled project links shown on PyPI.scriptsandgui-scripts: console-script entry points.mypackage = "mypackage.cli:main"creates amypackagecommand that callsmain()inmypackage.cliat install time.entry-points: named entry-point groups for plugin systems.dynamic: a list of field names whose values the build backend computes at build time. Common uses includeversion(with hatch-vcs or setuptools-scm) andreadme.
Dependency groups (PEP 735)
For development-only dependencies that should not ship as extras, use [dependency-groups] as specified by PEP 735:
[dependency-groups]
dev = [
"pytest>=7.0",
"ruff",
"mypy",
]
docs = [
"sphinx",
"furo",
]Dependency groups are a top-level table, not part of [project]. See What are optional dependencies and dependency groups? for the distinction between extras and groups, and Understanding dependency groups in uv for how uv installs them with uv sync --group.
The [build-system] Table
The [build-system] table tells installers which build backend to use when creating a wheel or sdist from source. It is defined by PEP 517 and PEP 518.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"requires: packages that must be installed in an isolated environment before building the project. Usually contains the build backend itself.build-backend: the Python object that exposes the PEP 517 build hooks (build_wheel,build_sdist, and so on).backend-path: an optional list of directories the installer should add tosys.pathbefore importing the backend. Used for in-tree backends.
Common build backends include:
| Backend | build-backend |
Notes |
|---|---|---|
uv_build |
uv_build |
Default backend for uv init --lib and uv init --package. Ships with uv and is the fastest option for pure-Python projects. |
| Setuptools | setuptools.build_meta |
Default for legacy projects. Supports C extensions. |
| Hatchling | hatchling.build |
Default backend for Hatch; available in uv init --build-backend hatch. Fast and standards-focused. |
| Flit | flit_core.buildapi |
Minimal. Pure-Python projects only. |
| PDM | pdm.backend |
Ships with PDM but works standalone. |
| Poetry | poetry.core.masonry.api |
Required for Poetry-managed projects. |
| Maturin | maturin |
For projects with Rust extensions via PyO3. |
| scikit-build-core | scikit_build_core.build |
For CMake-based C/C++ extensions. |
See What is a build backend? for how backends fit into the build pipeline, and What is a build frontend? for the frontend tools (pip, uv, build) that invoke the backend.
The [tool.*] Namespace
The [tool] table is reserved for third-party tools. Each tool claims a sub-table under [tool.<name>] and defines its own schema. PEP 518 reserved the namespace but does not regulate what goes inside it; tools are free to use TOML however they like.
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
[tool.mypy]
strict = true
python_version = "3.12"
warn_unused_ignores = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
[tool.uv]
package = trueTools that read configuration from pyproject.toml include Ruff, Black, mypy, pyright, ty, pytest, Coverage.py, tox (via [tool.tox]), uv, Poetry, Hatch, and PDM.
The pytest table is named [tool.pytest.ini_options] (not [tool.pytest]) to preserve its pytest.ini schema inside pyproject.toml.
pyproject.toml vs setup.py
| pyproject.toml | setup.py | |
|---|---|---|
| Format | Declarative TOML | Executable Python |
| Build input | Static metadata read by any PEP 517 backend | Arbitrary code run at build time |
| Tool configuration | Centralized under [tool.*] |
Scattered across setup.cfg, .flake8, mypy.ini, etc. |
| PEP 621 metadata | Native | Not supported |
| Isolated builds | Required by PEP 517 | Optional |
| Status | Current standard | Legacy; still supported by setuptools as a fallback |
setup.py is not deprecated, but it is no longer the recommended entry point for new projects. Setuptools still supports setup.py for legacy use cases, and it remains necessary for custom build logic that cannot be expressed declaratively. Most projects should migrate. See How to migrate from setup.py to pyproject.toml and Should I run python setup.py commands?.
pyproject.toml vs requirements.txt
| pyproject.toml | requirements.txt | |
|---|---|---|
| Defines a project | Yes (name, version, metadata) | No |
| Declares dependencies | Yes, in [project.dependencies] |
Yes, as flat list |
| Separates dev from runtime | Yes, via optional-dependencies or dependency groups | No, typically split into multiple files |
| Used for deployment pinning | With a lockfile (uv.lock, PEP 751 pylock) | Yes, as a pinned export |
| Used by build backends | Yes | No |
The two formats solve different problems. pyproject.toml describes the project; requirements.txt describes an install. Most modern workflows declare dependencies in pyproject.toml and export a pinned requirements.txt (or lockfile) for deployment. See How to migrate from requirements.txt to pyproject.toml with uv and Why should I choose pyproject.toml over requirements.txt?.
Pros
- Single source of truth for project metadata, dependencies, and tool configuration
- Standardized by PEPs 517, 518, 621, and 735; not owned by any single tool
- Declarative format prevents arbitrary code execution at build time
- Supported by every modern Python build backend and package manager
- Replaces
setup.py,setup.cfg,MANIFEST.in,.flake8,mypy.ini,pytest.ini, and others
Cons
- Some legacy build logic still requires a
setup.pyshim - The
[tool.*]namespace is reserved but unregulated, so each tool invents its own schema - TOML does not support complex data structures, which limits what some tools can express
- The PEP 621 metadata spec does not cover every legacy setuptools field;
dynamicis often needed
Learn More
- How to migrate from setup.py to pyproject.toml
- How to migrate from requirements.txt to pyproject.toml with uv
- Why should I choose pyproject.toml over requirements.txt?
- What is PEP 517 / 518 compatibility?
- What is PEP 621 compatibility?
- What is PEP 735 (dependency groups)?
- What is a build backend?
- What is a build frontend?
- Understanding dependency groups in uv
- pyproject.toml specification (Python Packaging User Guide)
- PEP 518 — Build System Requirements
- PEP 621 — Storing Project Metadata in pyproject.toml
- TOML specification
- uv reference