pyproject.toml
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
pyproject.toml is the one file that every modern Python packaging tool reads. This page was written against pip 26.2.2,
Poetry 2.5.1 with poetry-core 2.5.0, build 1.6.1 and hatchling 1.32.4, and the builds below were run on CPython 3.14.6 on
2026-10-22. The introduction
to pyproject.toml shows a first example; this page goes through every table and key.
The file is TOML. It has three standard tables and one open namespace:
| Table | Purpose | Read by |
|---|---|---|
|
Which backend builds the project and what must be installed to run it (PEP 518, PEP 517). |
Every build frontend: pip, build, Poetry, uv. |
|
The project’s metadata: name, version, dependencies, entry points, … (PEP 621). |
The build backend, which turns it into core metadata; Poetry 2 also uses it for resolution. |
|
Named lists of requirements that are not published: test, lint and docs tools (PEP 735). |
pip ( |
|
Settings of one tool. The name is the tool’s PyPI name. |
That tool only: |
Source: PyPA: pyproject.toml specification.
The [build-system] table
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
requires-
A list of PEP 508 requirements that the frontend installs into the isolated build environment before calling the backend. It must include the backend itself. Pin a minimum (the oldest version with the features you use, here hatchling’s PEP 639 support from 1.27.0) and, for backends that break compatibility on purpose, an upper bound on the next major (
poetry-core>=2.0.0,<3.0.0is whatpoetry newwrites). build-backend-
The import path of the backend object,
moduleormodule:object. If it is missing, frontends fall back tosetuptools.build_meta:__legacy__, which runs asetup.pythe old way; always set it. backend-path-
A list of directories, relative to the project root, that are put on
sys.pathbefore importing the backend. It exists for in-tree backends (a project that is its own backend, such as a backend’s own repository) and should not be used otherwise.
The backends in common use, with the line each one needs, are listed on Build Backends.
The [project] table
Every key, its TOML type and the core metadata field the backend writes for it:
| Key | TOML type | Core metadata field | Notes |
|---|---|---|---|
|
string |
|
Required, and the only key that can never be dynamic. Compared after normalization. |
|
string |
|
Required unless listed in |
|
string |
|
One line. |
|
string or table |
|
A path ( |
|
string |
|
A version specifier such as |
|
string |
|
An SPDX licence expression ( |
|
array of strings |
|
Glob patterns relative to the project root; each match goes into |
|
array of tables |
|
Tables with |
|
array of strings |
|
Joined with commas. |
|
array of strings |
|
Trove classifiers. |
|
table |
|
Label to URL. Well-known labels ( |
|
table |
|
Console and GUI launchers: |
|
table of tables |
|
Other entry-point groups, used for plugins. |
|
array of strings |
|
PEP 508 requirements needed at run time. |
|
table of arrays |
|
Extras, installed with |
|
array of strings |
|
PEP 794 (Accepted), see below. |
|
array of strings |
|
Keys the backend will fill in. |
The meaning of license, license-files and the classifiers, and how PyPI shows them, is covered in depth on
Preparing a Library. The requirement syntax
used by dependencies and optional-dependencies is on
Versions & Dependency Specifiers.
The core metadata it becomes
The hatchling wheel of the birdtally file at the end of this page contains this METADATA; the README follows it as the
message body:
Metadata-Version: 2.5
Name: birdtally
Version: 0.1.0
Summary: Tally bird-survey sightings from CSV files.
Project-URL: Homepage, https://example.org/birdtally
Project-URL: Changelog, https://example.org/birdtally/changelog
Author: The birdtally authors
License-Expression: MIT
License-File: LICENSE
Keywords: birds,csv,survey
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: pretty
Requires-Dist: rich>=13; extra == 'pretty'
Description-Content-Type: text/markdown
The poetry-core wheel of the same metadata says Metadata-Version: 2.4 and spells the requirement the older way,
Requires-Dist: rich (>=13) ; extra == "pretty"; both are valid. Core Metadata 2.6 (PEP 808, May 2026) is the current
version of the specification; backends are allowed to write the lowest version that holds every field they use, which is why
neither writes 2.6 for this project. Version 2.4 added License-Expression and License-File (PEP 639), 2.5 added
Import-Name and Import-Namespace (PEP 794), and 2.6 changed what Dynamic means for multiple-use fields (below). Tools
must refuse metadata with a higher major version than they know, and should warn on a higher minor one; twine 7.0.0 accepts
1.0 to 2.5.
Static and dynamic metadata
Most values are written directly in [project]: they are static, so any tool can read them without running the backend. A
key listed in dynamic is instead computed by the backend at build time, for example a version read from a Git tag or from
__version__. The rules come from PEP 621 and were extended by PEP 808:
-
namecan never be dynamic. A required field (name,version) must be either static or listed indynamic. -
If a key is not in
dynamic, the backend must use the static value and must not fill it in itself. -
If a key is in
dynamicand the backend cannot determine it, the build fails. -
For a single-value key, giving a static value and listing it in
dynamicis an error. -
Since PEP 808 (May 2026) a key whose value is a list or a table of arbitrary entries may be static and dynamic at the same time. The backend may then only append to the static value; it must not remove, reorder or change the entries you wrote. The keys are
authors,classifiers,dependencies,entry-points,gui-scripts,import-names,import-namespaces,keywords,license-files,maintainers,optional-dependencies,scriptsandurls.
Dynamic metadata has a cost: tools such as Poetry, uv and Dependabot cannot read a dynamic dependencies list without building
the project. Keep dependencies static.
A dynamic version with hatchling reads __version__ from the package:
[project]
name = "birdtally"
dynamic = ["version"]
[tool.hatch.version]
path = "src/birdtally/__init__.py"
Listing version statically and in dynamic fails the build with hatchling’s message:
ValueError: Metadata field `version` cannot be both statically defined and listed in field `project.dynamic`
and listing name in dynamic fails with:
ValueError: Static metadata field `name` cannot be present in field `project.dynamic`
In the sdist’s PKG-INFO, Dynamic: version tells installers that a wheel built from the sdist may have a different version
field. Since Core Metadata 2.6, a multiple-use field that is present in the sdist and also marked Dynamic may only be appended
to in a wheel built from it; before 2.6 such a field was ignored in the sdist.
Poetry has its own dynamic-version mechanism through plugins; see Versioning & Release Management.
Import names and import namespaces
import-names lists the import names the project exclusively provides, and import-namespaces the namespace packages it
contributes to without owning them (PEP 794, added to the specification in October 2025). A name followed by ; private marks
a private module, an empty list declares a distribution without Python modules, and the same name in both lists is an error.
The point is to let tools answer "which project provides import PIL?" from metadata:
[project]
name = "birdtally"
import-names = ["birdtally"]
With hatchling 1.32.4 this becomes Import-Name: birdtally in a Metadata-Version: 2.5 file. poetry-core 2.5.0 accepted the
key on the day of the check but did not write the field, so do not rely on it with the Poetry flavour yet.
Source: PyPA: import-names, PyPA: import-namespaces.
Extras and self-referential extras
An extra is a named set of optional dependencies. An extra may refer to other extras of the same project, which is the usual
way to offer an all extra:
[project.optional-dependencies]
pretty = ["rich>=13"]
cli = ["click>=8"]
all = ["birdtally[pretty,cli]"]
The backend expands it, so the wheel’s metadata lists the dependencies themselves:
Provides-Extra: all
Requires-Dist: click>=8; extra == 'all'
Requires-Dist: rich>=13; extra == 'all'
Provides-Extra: cli
Requires-Dist: click>=8; extra == 'cli'
pip has accepted self-referential extras since 21.2 and supports them officially since 26.2; the specification documents them since August 2026.
The [dependency-groups] table
Extras are part of the published package. Development tools are not: nobody who installs birdtally needs pytest. Before PEP
735 every tool invented its own place for them (requirements-dev.txt, [tool.poetry.dev-dependencies], [tool.pdm.dev-dependencies]).
[dependency-groups] standardises it:
[dependency-groups]
test = ["pytest>=8"]
lint = ["ruff>=0.6"]
docs = ["mkdocs>=1.6"]
dev = [{ include-group = "test" }, { include-group = "lint" }]
-
Each value is a list of PEP 508 strings or
{include-group = "<name>"}tables. Includes are expanded recursively; a cycle is an error. Group names are normalized like project names. -
A group is not a requirement on the project itself. To test the installed project, install the project and the group (
pip install . --group test). -
Build backends must not put dependency groups into the metadata of sdists or wheels. They stay in the copy of
pyproject.tomlinside the sdist, but they are not part of the package’s interface.
How pip installs groups is on pip: Dependency Groups & Inline Script Metadata, and how Poetry maps them to its own groups is on Poetry: Managing Dependencies & Groups. The tools that the groups name are covered in Testing, Debugging & Tooling and Docstrings.
The [tool] table and who reads what
[tool.<name>] is reserved for the tool whose PyPI name is <name>. No standard governs what is inside. The tools in this
section read the file like this:
| Tool | What it reads |
|---|---|
pip |
|
Poetry 2.x |
|
hatchling |
|
setuptools |
|
The complete birdtally file, both flavours
The two variants of the running scenario have the same code and the same metadata. The left-hand file builds with hatchling and
is used with pip, build and twine; the right-hand file builds with poetry-core and is managed by Poetry. Both poetry check and
python -m build accept them.
| pip flavour (hatchling) | Poetry flavour (poetry-core) |
|---|---|
|
|
The differences are deliberate:
-
[build-system]names the backend. poetry-core is pinned below 3 because a major version is where Poetry puts the breaking changes it cannot phase in through deprecations (Poetry does not claim strict semantic versioning). -
[tool.poetry]adds what[project]cannot express. hatchling findssrc/birdtallyon its own; poetry-core is told withpackages, andincludeaddsCHANGELOG.mdto the sdist only. Without it the Poetry sdist would leave the changelog out, while hatchling’s sdist includes every file that Git does not ignore (includingtests/andexamples/). -
The Python classifier
Programming Language :: Python :: 3is written out in the hatchling file and left out of the Poetry one. With a staticclassifierslist poetry-core writes exactly what you list. If you instead move the list to[tool.poetry] classifiersand declaredynamic = ["classifiers"], poetry-core appends `Programming Language-
Python :: 3` and one
3.xclassifier per minor version allowed byrequires-python(3.10 to 3.15 for>=3.10with poetry-core 2.5.0); see Poetry: pyproject.toml & Project Metadata.
Common mistakes
-
Missing
build-backend. The legacy setuptools fallback runs and the project may build with the wrong backend. -
Putting dev tools in
optional-dependencies. They are then published as an extra; use[dependency-groups]. -
Declaring a dependency in both
[project]and[tool.poetry.dependencies]with different constraints. Poetry 2 uses the[tool.poetry]entry only to enrich the[project]one (source, develop mode, …); keep the version constraint in one place. -
Using caret or tilde constraints in
[project.dependencies]. They are Poetry syntax and are not valid PEP 508; see the translation table on Versions & Dependency Specifiers. -
A
license-filesglob that matches nothing. Withlicense-files = ["LICENCE.txt"](a misspelling of the realLICENSE)poetry buildstops withNo files found for license file glob pattern 'LICENCE.txt'and exit code 1, but hatchling 1.32.4 built the wheel without any licence file and without aLicense-Filefield, and printed no warning. Check the wheel withpython -m zipfile -l dist/*.whlbefore publishing. -
An upper bound on
requires-python. Resolvers then refuse to install the project on newer Pythons, and Poetry propagates the bound to every dependent project.