Migrating to Poetry 2

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.

Poetry 2.0 (2025-01-05) made the standard [project] table the place for project metadata and changed several commands. This page migrates a Poetry 1.x project to Poetry 2.5.1, the release this section is written against, with every step run on 2026-10-22 on Linux with CPython 3.14.6: birdtally was first locked with Poetry 1.8.5, then taken over by Poetry 2.5.1. Scratch paths are shortened to ….

What keeps working

Poetry 2.5.1 still reads the 1.x layout and lock files, so a project can be migrated in steps:

  • poetry install installed the project from its Poetry 1.8.5 lock file (lock-version = "2.0") without a complaint.

  • poetry check accepts the old [tool.poetry] metadata but warns about every field (shown on pyproject.toml & Project Metadata); exit code 0, so only --strict fails on it.

  • Builds still work: poetry-core 2.x reads both layouts.

Step 1: relock

The release post explains this change under Locking resulting markers and groups.

poetry lock in 2.x keeps every locked version (the old --no-update behaviour is now the default) and rewrites the file in format 2.1, which records for each package the groups it belongs to and the markers under which it is needed:

poetry lock
head -1 poetry.lock
Resolving dependencies...

Writing lock file
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
< # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
> # This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
> groups = ["dev"]
> markers = "sys_platform == \"win32\""
…
> groups = ["main", "dev"]
> markers = {main = "extra == \"pretty\""}

(an excerpt of diff between the two lock files, hashes left out). Every package kept its version; the metadata table now says lock-version = "2.1". With a 2.1 lock file and installer.re-resolve = false (the default since 2.3), installing no longer resolves again but evaluates those markers. Commit the relocked file on its own, so that the metadata change is a separate, reviewable step.

Step 2: move the metadata to [project]

The Poetry 1.x file:

[tool.poetry]
name = "birdtally"
version = "0.1.0"
description = "Tally bird-survey sightings from CSV files."
authors = ["The birdtally authors <birds@example.org>"]
license = "MIT"
readme = "README.md"
homepage = "https://example.org/birdtally"
keywords = ["birds", "survey", "csv"]
packages = [{ include = "birdtally", from = "src" }]

[tool.poetry.dependencies]
python = "^3.10"
rich = { version = "^13", optional = true }

[tool.poetry.extras]
pretty = ["rich"]

[tool.poetry.scripts]
birdtally = "birdtally.cli:main"

[tool.poetry.group.dev.dependencies]
pytest = "^8"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

The same project for Poetry 2:

[project]
name = "birdtally"
version = "0.1.0"
description = "Tally bird-survey sightings from CSV files."
authors = [{ name = "The birdtally authors", email = "birds@example.org" }]
license = "MIT"
license-files = ["LICENSE"]
readme = "README.md"
keywords = ["birds", "survey", "csv"]
requires-python = ">=3.10,<4.0"
dependencies = []

[project.optional-dependencies]
pretty = ["rich (>=13,<14)"]

[project.urls]
homepage = "https://example.org/birdtally"

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

[dependency-groups]
dev = ["pytest (>=8,<9)"]

[tool.poetry]
packages = [{ include = "birdtally", from = "src" }]
requires-poetry = ">=2.4.2,<3.0"

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

Field by field:

Poetry 1.x Poetry 2 Note

name, version, description, readme, keywords, classifiers

the same keys in [project]

With no classifiers (as here) poetry-core still adds Programming Language :: Python :: 3.x ones; a static [project] list is used as it is.

authors = ["Name <email>"]

authors = [{ name = "…", email = "…" }]

maintainers likewise.

license = "MIT"

license = "MIT" and license-files

A PEP 639 SPDX expression; poetry-core 2.2 writes License-Expression: MIT into the metadata.

python = "^3.10" in the dependencies

requires-python = ">=3.10,<4.0"

PEP 440 has no caret: ^3.10 means >=3.10,<4.0. Writing only >=3.10 changes what the solver must cover and can change the lock.

rich = { version = "^13", optional = true } plus [tool.poetry.extras]

[project.optional-dependencies] pretty = ["rich (>=13,<14)"]

Translate every ^ and ~ by hand (see Dependency Specification).

other [tool.poetry.dependencies]

[project] dependencies

Poetry-only details (source, develop, allow-prereleases) stay in [tool.poetry.dependencies] next to the PEP 508 entry.

[tool.poetry.scripts]

[project.scripts]

Scripts of type file stay in [tool.poetry.scripts].

homepage, repository, documentation, [tool.poetry.urls]

[project.urls]

[tool.poetry.group.dev.dependencies]

[dependency-groups] (PEP 735, Poetry 2.2), or keep the Poetry group

Optional groups (optional = true) exist only as Poetry groups.

poetry-core>=1.0.0

poetry-core>=2.2.0,<3.0.0

poetry-core 1.x cannot read [project]; this file uses PEP 639 license-files, supported since 2.2.

Three ways to declare dependencies

[tool.poetry.dependencies] is not deprecated, because it can express things [project] dependencies cannot. The 2.0 announcement and the FAQ describe three options:

  1. Standard only: move everything to [project] dependencies (what birdtally does). Best when no Poetry-specific feature is used.

  2. Standard plus enrichment: keep the PEP 508 entries in [project] dependencies and add Poetry details (a source, develop = true, allow-prereleases) for the same names in [tool.poetry.dependencies]. Poetry may not add dependencies there, only describe them.

  3. Dynamic: declare dynamic = ["dependencies"] in [project] and keep using [tool.poetry.dependencies] alone, for projects that rely heavily on Poetry’s syntax.

include now defaults to the sdist

Since 2.0, a [tool.poetry] include entry without format goes into the sdist only (before, files went into both distributions and directories into the sdist). Files that must also be in the wheel need format = ["sdist", "wheel"]; be explicit everywhere.

Always keep [build-system]

Without a [build-system] table, Poetry 2.5.1 still builds with poetry-core but warns:

WARNING: No build backend defined. Please define one in the pyproject.toml.
Falling back to using the built-in `poetry-core` version.
In a future release Poetry will fallback to `setuptools` as defined by PEP 517.
More details can be found at https://python-poetry.org/docs/libraries/#packaging

Every release post from 2.1 to 2.5 announces that change, and poetry check does not report the missing table. Since 2.1, poetry build also honours another back end named there (building in an isolated environment) and accepts --config-settings.

Relock and validate

Relock (the content hash covers the new tables) and validate strictly:

poetry check --strict
poetry lock
poetry check --strict
Error: pyproject.toml changed significantly since poetry.lock was last generated. Run `poetry lock` to fix the lock file.
Resolving dependencies...

Writing lock file
All set!

The relock kept every version, and the wheel built from the new file carries the same requirements:

Metadata-Version: 2.4
Name: birdtally
Version: 0.1.0
Summary: Tally bird-survey sightings from CSV files.
License-Expression: MIT
License-File: LICENSE
Keywords: birds,survey,csv
Author: The birdtally authors
Author-email: birds@example.org
Requires-Python: >=3.10,<4.0
Classifier: Programming Language :: Python :: 3
…
Classifier: Programming Language :: Python :: 3.15
Provides-Extra: pretty
Requires-Dist: rich (>=13,<14) ; extra == "pretty"
Project-URL: Homepage, https://example.org/birdtally

Step 3: commands and options

Poetry 1.x Poetry 2.5.1

poetry shell

Plugin poetry-plugin-shell; use eval $(poetry env activate) instead. Without the plugin: Since Poetry (2.0.0), the shell command is not installed by default.

poetry export

Plugin poetry-plugin-export (poetry self add poetry-plugin-export or [tool.poetry.requires-plugins]). Without it: The requested command export does not exist.

poetry install --sync

poetry sync; --sync still works with The --sync option is deprecated and slated for removal, use the poetry sync command instead.

poetry lock --no-update

The default; the option is gone: The option "--no-update" does not exist. The old default is poetry lock --regenerate.

poetry lock --check

poetry check --lock (since 1.6): The option "--check" does not exist.

poetry add --optional rich

poetry add rich --optional=pretty: the option now takes the extra. The old form swallows the package name: Not enough arguments (missing: "name").

poetry -C dir …

Now really changes to dir; -P dir (--project) uses the project there but resolves paths from the current directory.

poetry new (flat layout)

The src layout by default since 2.1; --flat for the old one.

After 2.0: the later steps

Release What to do

2.1

Pass --priority to every poetry source add; expect the src layout from poetry new.

2.2

Optionally move groups to PEP 735 [dependency-groups] and the licence to an SPDX license with license-files.

2.3

With PEP 735 groups, require Poetry 2.3 or later (requires-poetry = ">=2.3"): 2.2.x considers a 2.3 lock file outdated, because 2.3 started hashing those groups. Installing now evaluates the locked markers (installer.re-resolve = false); set it to true to get the old behaviour back if something differs.

2.4

Consider a cooldown with solver.min-release-age.

2.5

Try installer.builtin-uninstall = true; it is planned to become the default.

Step 4: settings

  • virtualenvs.prefer-active-python became its inverse, virtualenvs.use-poetry-python (default false: the active Python is used, as prefer-active-python = true did).

  • experimental.system-git-client became system-git-client.

  • virtualenvs.options.no-setuptools is gone; environments never get setuptools by default.

poetry config --migrate (and --migrate --local for poetry.toml) rewrites the renamed ones; see Configuration. (The 2.0.0 changelog calls the new name experimental.system-git; the release post and the configuration documentation say system-git-client, which is what 2.5.1 lists.)

Checklist

  1. Upgrade Poetry everywhere, to 2.4.2 or later for the security fixes (see Versions & Release History), and pin it in CI.

  2. poetry lock, commit.

  3. Move the metadata to [project], raise poetry-core in [build-system], add requires-poetry.

  4. poetry lock, poetry check --strict, build, and compare the wheel’s METADATA with the last release’s.

  5. Replace poetry shell, install --sync, lock --no-update, lock --check and add --optional in scripts, Makefiles, Dockerfiles and CI; add the export plugin where poetry export is used.

  6. poetry config --migrate on every machine and in CI images.