pyproject.toml & Project Metadata

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.

Since Poetry 2.0 a Poetry project describes itself with the standard [project] table, and keeps a [tool.poetry] table only for what the standard cannot express. This page goes through both, as Poetry 2.5.1 and poetry-core 2.5.0 read them; the commands ran on CPython 3.14.6 on 2026-10-22. The standard keys themselves, and the core metadata each becomes, are on pyproject.toml; this page adds what is specific to Poetry.

The [project] section as Poetry reads it

Poetry reads every PEP 621 key. The points that differ from other backends:

Key Poetry specifics

name

Required in package mode. Optional with package-mode = false.

version

Required whenever [project] exists, unless listed in dynamic. For a version set by poetry build --local-version or by a plugin, put version in dynamic and the base version in [tool.poetry] version.

description

One line.

license

An SPDX expression string (poetry-core supports PEP 639 since 2.2). The table form {text = …} / {file = …} is deprecated.

license-files

Globs; a pattern that matches nothing fails poetry build.

readme

One file. For several files concatenated, see [tool.poetry] readme.

requires-python

The Python range of the project, written into the metadata. To lock with an upper bound (<4.0) without publishing one, omit it here and add python = ">=3.10,<4.0" under [tool.poetry.dependencies]; that entry may only narrow requires-python.

authors, maintainers

Tables with name and email.

keywords, classifiers, urls

As in the standard. With static classifiers, Poetry adds nothing.

scripts, gui-scripts, entry-points

Console and GUI scripts and plugin entry points. Run poetry install after changing them.

dependencies, optional-dependencies

PEP 508 strings, the source of the metadata. [tool.poetry.dependencies] can enrich them for locking (see below).

A dynamic version with a base version:

[project]
name = "birdtally"
dynamic = ["version"]

[tool.poetry]
version = "0.1.0"   # base version
poetry build -f wheel --local-version birdwatch.1
  - Building wheel
  - Built birdtally-0.1.0+birdwatch.1-py3-none-any.whl

A local version is fine for an internal index, but PyPI refuses it; see Versioning & Release Management.

The [tool.poetry] section

package-mode

package-mode = false turns a project into a dependency-only application: no build, no publish, name and version optional, and poetry install installs only the dependencies. See Basic Usage.

packages

Poetry auto-detects one package or module whose name is the normalized project name with - replaced by _, either next to pyproject.toml (flat layout) or under src/ (src layout). Anything else needs packages, and as soon as you set it auto-detection is off, so list the main package too:

[tool.poetry]
packages = [
    { include = "birdtally", from = "src" },             # the main package, from src/
    { include = "birdtally_plugins", from = "lib", to = "birdtally/plugins" },   # installed under another path
    { include = "birdtally_fixtures", format = "sdist" },  # only in the sdist
]
  • include is a package, module or glob (extra_package/**/*.py); subpackages are found automatically.

  • from is the directory that contains it.

  • to (1.8) is the destination path inside the installed distribution.

  • format restricts it to sdist or wheel (or a list).

The .gitignore caveat. In a Git (or other VCS) checkout, files matched by the ignore rules are excluded from the built distributions even when their directory is listed in packages`. Generated code that you keep out of Git (a `_version.py, a compiled parser) therefore silently goes missing; add it back with include and an explicit format.

Source: Poetry: packages.

include and exclude

[tool.poetry]
include = [
    { path = "CHANGELOG.md", format = "sdist" },
    { path = "src/birdtally/data/*.csv", format = ["sdist", "wheel"] },
]
exclude = ["src/birdtally/_scratch.py"]
  • include has priority over exclude.

  • Without format, include applies only to the sdist (since Poetry 2.0; before, it applied to both), while exclude applies to both. Files meant for the installed package (data files) need format = ["sdist", "wheel"].

  • Top-level paths in a wheel are unpacked straight into site-packages, so never ship CHANGELOG.md, LICENSE, tests or docs at the top level of a wheel; licence files belong in license-files.

  • exclude is seeded with the VCS ignore rules; an include entry with an explicit format can override them.

readme and classifiers

[tool.poetry] readme accepts a list of files that are concatenated into the long description (readme = ["README.md", "docs/usage.md"]); use it with dynamic = ["readme"]. [tool.poetry] classifiers with dynamic = ["classifiers"] makes poetry-core add Programming Language :: Python :: 3 and one 3.x classifier per minor version allowed by requires-python; with a static [project] classifiers list nothing is added. Both behaviours are shown on pyproject.toml.

scripts of type file

Console scripts belong in [project.scripts]. [tool.poetry.scripts] remains for scripts of type = "file": a file from the project copied into bin/ (Scripts\ on Windows) at install time:

[tool.poetry.scripts]
birdtally-import = { reference = "scripts/birdtally-import.sh", type = "file" }

requires-poetry, requires-plugins and build-constraints

[tool.poetry]
requires-poetry = ">=2.3"

[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.10"

[tool.poetry.build-constraints]
some-package = { setuptools = "<78" }
  • requires-poetry (2.0) makes every command fail fast on an unsuitable Poetry. With requires-poetry = ">=3.0":

    This project requires Poetry >=3.0, but you are using Poetry 2.5.1
  • requires-plugins (2.0) lists project plugins that Poetry installs into the project’s .poetry/plugins directory when they are missing; see Plugins.

  • build-constraints (2.3) constrains the build environment of a dependency that Poetry has to build from source, for example an sdist that requires an unbounded setuptools it is not compatible with. The syntax is that of [tool.poetry.dependencies].

Dependencies and groups in [tool.poetry]

[tool.poetry.dependencies] and [tool.poetry.group.<name>.dependencies] still exist. Next to [project] they enrich the standard dependencies for locking (a source, develop = true, allow-prereleases) and can hold Poetry-only syntax; see Dependency Specification and Managing Dependencies & Groups.

Deprecated [tool.poetry] fields

Deprecated [tool.poetry] field Use instead

name

project.name

version

project.version (keep tool.poetry.version only as the base of a dynamic version)

description

project.description

license

project.license (SPDX string)

authors, maintainers ("Name <email>" strings)

project.authors, project.maintainers (tables)

homepage, repository, documentation

project.urls

keywords

project.keywords

urls

project.urls

extras

project.optional-dependencies

plugins

project.entry-points

scripts (console)

project.scripts

dependencies.python as the only Python requirement

project.requires-python (keep python there only to narrow the range for locking)

dev-dependencies

a group ([dependency-groups] dev or [tool.poetry.group.dev.dependencies])

tool.poetry.readme and tool.poetry.classifiers are not deprecated, but they need the key in project.dynamic when [project] exists.

poetry check

poetry check validates pyproject.toml (and, with --lock, that poetry.lock is consistent with it) and lists every deprecated field. Run on a Poetry 1.x style file for birdtally:

poetry check
Warning: [tool.poetry.name] is deprecated. Use [project.name] instead.
Warning: [tool.poetry.version] is set but 'version' is not in [project.dynamic]. If it is static use [project.version]. If it is dynamic, add 'version' to [project.dynamic].
If you want to set the version dynamically via `poetry build --local-version` or you are using a plugin, which sets the version dynamically, you should define the version in [tool.poetry] and add 'version' to [project.dynamic].
Warning: [tool.poetry.description] is deprecated. Use [project.description] instead.
Warning: [tool.poetry.readme] is set but 'readme' is not in [project.dynamic]. If it is static use [project.readme]. If it is dynamic, add 'readme' to [project.dynamic].
If you want to define multiple readmes, you should define them in [tool.poetry] and add 'readme' to [project.dynamic].
Warning: [tool.poetry.license] is deprecated. Use [project.license] instead.
Warning: [tool.poetry.authors] is deprecated. Use [project.authors] instead.
Warning: [tool.poetry.keywords] is deprecated. Use [project.keywords] instead.
Warning: [tool.poetry.homepage] is deprecated. Use [project.urls] instead.
Warning: [tool.poetry.extras] is deprecated. Use [project.optional-dependencies] instead.
Warning: Defining console scripts in [tool.poetry.scripts] is deprecated. Use [project.scripts] instead. ([tool.poetry.scripts] should only be used for scripts of type 'file').

The exit code is 0: warnings do not fail the check. poetry check --strict turns warnings into errors (exit code 1), which is what to run in CI. The full migration of that file is on Migrating to Poetry 2.

[build-system] with poetry-core

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

This is what poetry new and poetry init write. Any PEP 517 frontend (pip, build, uv) can then build the project without Poetry. The documentation’s own snippet still shows poetry-core>=1.0.0, which would allow a poetry-core 1.x that cannot read [project]; keep the 2.0 lower bound. A pyproject.toml that references poetry itself as the backend must be changed to poetry-core. The Poetry 2.1 to 2.5 release notes announce that a future release will default to setuptools when [build-system] is missing, following PEP 517, so always write the table.

The complete birdtally file

The Poetry flavour of the running scenario, which poetry check accepts with All set! and from which the published metadata on pyproject.toml is built:

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[project]
name = "birdtally"
version = "0.1.0"
description = "Tally bird-survey sightings from CSV files."
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "The birdtally authors" }]
keywords = ["birds", "survey", "csv"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Typing :: Typed",
]
dependencies = []

[project.optional-dependencies]
pretty = ["rich>=13"]

[project.scripts]
birdtally = "birdtally.cli:main"

[project.urls]
Homepage = "https://example.org/birdtally"
Changelog = "https://example.org/birdtally/changelog"

[tool.poetry]
packages = [{ include = "birdtally", from = "src" }]
include = [{ path = "CHANGELOG.md", format = "sdist" }]

[dependency-groups]
test = ["pytest>=8"]
lint = ["ruff>=0.6"]
docs = ["mkdocs>=1.6"]
dev = [{ include-group = "test" }, { include-group = "lint" }]