pyproject.toml Explained: Sections and Examples

What pyproject.toml is and what goes in it: build-system, project metadata, dependencies, dependency groups and tool settings, with a tested example.

pyproject.toml is the single configuration file for a modern Python project: it says how to build the package, describes it and its dependencies, and holds settings for your tools. For new projects it replaces setup.py and setup.cfg, which the Python Packaging User Guide says to keep only if you need programmatic configuration.

The three tables at a glance

TablePurposeStatus according to the PyPA guide
[build-system]Names the build backend and what is needed to build the projectStrongly recommended, and should always be present
[project]Name, version, dependencies, scripts and other metadataUnderstood by most build backends; some use a different format
[tool]Settings for individual tools, for example [tool.ruff]Contents are defined by each tool

A complete example

This example was built and exercised for real: installed with pip, built into a wheel and sdist with python -m build and uv build, synced with uv, and its tools run against it.

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

[project]
name = "mypkg"
version = "0.1.0"
description = "Does something useful"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
dependencies = ["httpx>=0.27", "pydantic>=2"]

[project.optional-dependencies]
cli = ["typer"]

[project.scripts]
mypkg = "mypkg.cli:app"

[dependency-groups]
dev = ["pytest", "ruff"]

[tool.ruff]
line-length = 100

[tool.pytest.ini_options]
testpaths = ["tests"]

[build-system]

The PyPA guide calls this table strongly recommended and says it should always be present, because it defines the build tool. build-backend names the backend and requires lists what must be installed to build the project, usually just the backend. The guide’s values for common backends are Hatchling (hatchling >= 1.26), setuptools (setuptools >= 77.0.3), Flit, PDM and uv-build; those minimum versions are the ones that added support for the license string used above. For a comparison, see Hatch vs uv.

If the table is missing, pip’s documentation describes the fallback: a project with a setup.py and no build-system section is assumed to use setuptools>=40.8.0 with the setuptools.build_meta:__legacy__ backend. A build-system section without a build-backend is treated the same way.

[project]

The [project] table is understood by most build backends. The PyPA guide notes that Poetry only started to support it in version 2.0, released on 5 January 2025, having used [tool.poetry] before. Two fields are required: name, the only field that cannot be dynamic, and version, which is often declared dynamic so the build backend fills it from a __version__ attribute or a Git tag with dynamic = ["version"].

Other fields used in the example:

  • requires-python is the minimum Python version you support.
  • dependencies uses dependency specifiers such as httpx>=0.27, including environment markers like django>2.1; os_name != 'nt'.
  • readme is a file name; the format is detected from the extension (.md is GitHub-flavoured Markdown, .rst is reStructuredText).
  • license is an SPDX licence expression string such as "MIT". The older table form with file or text is the legacy specification, and license-files lists licence files to include. Building the example gives License-Expression: MIT in the wheel’s metadata.
  • authors and maintainers are lists of tables with name and email.

Optional dependencies and scripts

[project.optional-dependencies] defines extras, which users install with pip install "mypkg[cli]". In the built wheel’s metadata this appears as Provides-Extra: cli and Requires-Dist: typer; extra == 'cli', and the base install does not pull in typer. An extra can refer back to its own project, for example all = ["mypkg[gui, cli]"].

[project.scripts] creates command-line entry points. After installing the example, running mypkg calls app in mypkg/cli.py and prints hello from mypkg; the wheel contains an entry_points.txt with [console_scripts]. [project.gui-scripts] differs only on Windows, where it avoids opening a terminal window.

[dependency-groups]

Dependency groups (PEP 735) hold requirements that should not become part of the published package: the specification describes them as suitable for linting and testing, and for projects that are never built for distribution. Groups can include each other with {include-group = "name"}.

  • Not in the built package. The example wheel’s metadata lists httpx, pydantic and the cli extra, but not pytest or ruff.
  • pip 25.1 and later: pip install --group dev installs the group from pyproject.toml in the current directory. pip does not search for the file.
  • uv: uv sync installs the dev group by default, and uv sync --no-dev leaves it out. uv sync --extra cli adds an extra. See what uv is.

Older projects put these tools in an extra or a separate requirements-dev.txt.

[tool.*]

Each tool gets its own namespace, and the PyPA guide notes that its contents are defined by each tool. Both tools in the example read their sections: with line-length = 100, ruff check --show-settings reports linter.line_length = 100, and pytest picks up testpaths = ["tests"] and collects the test there. Type checkers read their own tables; see Python type checkers compared.

What about lock files?

pyproject.toml says what you want (httpx>=0.27); a lock file records what was actually resolved, with exact versions and hashes. uv writes uv.lock and Poetry writes poetry.lock. There is also a standard format, pylock.toml, defined by a PyPA specification that originated as PEP 751. pip’s experimental pip lock and uv export --format pylock.toml both write it. See Python package managers compared and uv vs pip.

Where it fits

If you are choosing a project layout, read src layout vs flat layout. To publish what you have built, see how to publish a Python package to PyPI.

Frequently Asked Questions

What is pyproject.toml?

pyproject.toml is the standard configuration file for Python projects. It can hold three tables: [build-system], which says how to build the project, [project], which holds its metadata and dependencies, and [tool], which holds settings for tools such as Ruff, pytest and mypy.

Do I need pyproject.toml?

For anything you build, install or publish, yes. The Python Packaging User Guide strongly recommends the [build-system] table and tells new projects to use the [project] table, keeping setup.py only if you need programmatic configuration. A loose script does not need one: uv can run a script with its dependencies declared inside the file.

What are the main sections of pyproject.toml?

[build-system] names the build backend and what it needs, [project] holds name, version, requires-python, dependencies, optional dependencies and scripts, and [tool.*] tables hold configuration for individual tools, for example [tool.ruff] or [tool.pytest.ini_options].

What are dependency groups?

A [dependency-groups] table lists named sets of requirements, such as test or dev, that are not included in the metadata of the built package. pip 25.1 and later installs them with pip install —group, and uv sync installs the dev group by default.

Is pyproject.toml replacing requirements.txt?

For declaring a project’s dependencies, largely yes. For recording exact versions, lock files do that job: uv writes uv.lock, Poetry writes poetry.lock, and pylock.toml is a standard lock file format that pip lock and uv export can write.