10aday › tools

Poetry to PEP 621 converter: ^ and ~ to [project] dependencies

Since Poetry 2, dependencies can live in the standard [project] table, which pip, uv, Hatch and PDM also read. That table uses PEP 508 strings, and PEP 440 has no caret (^) and no tilde (~). Paste your pyproject.toml (or just the [tool.poetry.dependencies] lines) and get the equivalent [project] table: explicit bounds, python = … turned into markers, extras and groups carried over.

One constraint

Whole file


  

Everything runs in your browser; nothing is sent anywhere. Keep [build-system], scripts and other [tool.poetry] settings as they are.

Cheat sheet: Poetry operators in PEP 440

PoetryMeaningPEP 440 / [project]
^1.2.3no change to the first non-zero number>=1.2.3,<2.0.0
^0.2.30.x: the minor is the breaking number>=0.2.3,<0.3.0
^0.0.30.0.x: only that patch line>=0.0.3,<0.0.4
^1.2 / ^1same rule, shorter bound>=1.2,<2.0 / >=1,<2
~1.2.3patch updates>=1.2.3,<1.3.0 (same as ~=1.2.3)
~1.2patch updates>=1.2,<1.3 (not ~=1.2, which is <2.0)
~1any 1.x>=1,<2
~=1.4PEP 440 compatible release~=1.4 (unchanged)
~=1accepted by Poetry only>=1,<2 (pip rejects ~=1)
1.2.*, 1.*wildcard==1.2.*, ==1.*
"1.2.3" (bare)exact version==1.2.3
*any versionno specifier: just the name
>=1.2 <2space = AND>=1.2,<2
^1.2 || ^3.0ORno equivalent: rewrite by hand
python = "^3.9" (project)supported Pythonsrequires-python = ">=3.9,<4.0"
python = ">=3.10" (on a dependency)install only on those Pythonsmarker ; python_version >= "3.10"
platform = "win32"install only on that OSmarker ; sys_platform == "win32"
optional = true + [tool.poetry.extras]extra[project.optional-dependencies]
[tool.poetry.group.dev.dependencies]dependency group[dependency-groups] (PEP 735)

Not sure what your new requires-python allows? Check it with the requires-python checker (<=3.12 excludes 3.12.1, ==3.10 means 3.10.0 only).

Every Poetry constraint with its PEP 440 form, and the common ^ → ~= mistake: Poetry ^ and ~ to PEP 440.

Need the same comparison for npm, Cargo, Composer or RubyGems? Use the Version Constraint Checker or read pip ~= and PEP 440 specifiers and tilde vs caret.

Traps this converter flags

Common questions

Does Poetry 2 still read [tool.poetry.dependencies]? Yes. If both tables exist, [project.dependencies] is used for the package metadata, and the Poetry table only adds locking details such as sources. Moving to [project] makes the file readable by other tools (uv, pip, Hatch, PDM, Dependabot).

Should requires-python keep the <4.0 upper bound? The converter keeps exactly what ^3.9 meant in Poetry. Many projects drop the cap (>=3.9), because an upper bound in requires-python can make resolvers pick old versions of your package. Edit it if you prefer.

Are the markers the same as Poetry's? Yes, apart from the python = "3" case above: for python = …, the converter writes the markers Poetry itself writes (python_version for X.Y bounds, python_full_version when a patch number matters).

Moving from Poetry to uv

uv reads the same standard tables, so the dependency part of a Poetry-to-uv migration is this conversion. The rest, step by step:

  1. Paste your file above and copy the result into pyproject.toml, replacing [tool.poetry.dependencies], [tool.poetry.extras] and the [tool.poetry.group.*] tables.
  2. Keep a build backend that understands [project]: poetry-core>=2.0 works, or switch to uv_build or hatchling. Applications that are never built as a package can skip [build-system].
  3. Move path dependencies to [tool.uv.sources] (for example mylib = { path = "../mylib", editable = true }) and list the bare name in dependencies; private indexes go to [[tool.uv.index]].
  4. Run uv lock, then uv sync. uv installs the dev group by default; other groups need --group name.

We checked that the default example above, once converted, locks with uv 0.11 (35 packages resolved). For a whole-repository migration (lock file, scripts, authors), the open-source CLI migrate-to-uv does more; this page is for when you want to see and check each line.

Sources