Skip to content

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:

pyproject.toml
[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.

pyproject.toml
[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 in dynamic): a PEP 440-compliant version string, or dynamic = ["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 use license = { file = "LICENSE" } or license = { text = "MIT" }.
  • authors and maintainers: 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. Installing mypackage[dev] pulls in the dev list.
  • keywords and classifiers: PyPI search metadata.
  • urls: a table of labeled project links shown on PyPI.
  • scripts and gui-scripts: console-script entry points. mypackage = "mypackage.cli:main" creates a mypackage command that calls main() in mypackage.cli at 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 include version (with hatch-vcs or setuptools-scm) and readme.

Dependency groups (PEP 735)

For development-only dependencies that should not ship as extras, use [dependency-groups] as specified by PEP 735:

pyproject.toml
[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.

pyproject.toml
[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 to sys.path before 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.

pyproject.toml
[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 = true

Tools 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.py shim
  • 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; dynamic is often needed

Learn More

Last updated on