Skip to content

How to migrate from Poetry to uv

Poetry and uv both manage dependencies and virtual environments from a single tool, but uv adds Python version management, faster resolution, and a standards-first pyproject.tomlThe standard configuration file for Python projects. Declares the project name, version, dependencies, build system, and tool settings in one place. Learn more → layout. Migrating rewrites Poetry’s [tool.poetry] tables into PEP 621 project metadata, replaces poetry.lock with uv.lock, and swaps poetry commands for their uv equivalents.

One command handles most of the work. The rest of this guide shows what converts automatically, what needs manual cleanup, and how to fix the cases that trip people up.

Prerequisites

Convert the project with migrate-to-uv

migrate-to-uv reads a Poetry project and rewrites it for uv in place. Run it with uvx, which fetches and executes the tool without a permanent install.

1. Preview the changes

$ uvx migrate-to-uv --dry-run

--dry-run prints the converted pyproject.toml without touching any files. Read it before committing to the migration.

2. Run the migration

$ uvx migrate-to-uv
Locking dependencies with constraints from existing lock file(s) using "uv lock"...
Resolved 13 packages in 3ms
Locking dependencies again using "uv lock" to remove constraints...
Resolved 13 packages in 3ms
Successfully migrated project from Poetry to uv!

warning: Build backend was migrated to uv. It is highly recommended to check that files and data included in the source distribution and wheels are the same after the migration.

The tool rewrites pyproject.toml, generates uv.lock from the versions already pinned in poetry.lock, and removes the Poetry-specific files. That trailing warning appears on every successful run; it is a prompt to compare the built artifacts, not a failure.

Poetry’s own virtual environment survives the migration in its cache. Delete it with poetry env remove --all once uv sync works.

3. Verify the result

$ uv sync
$ uv run pytest

uv sync installs the locked dependencies into .venv. If both commands succeed, the migration worked.

What migrate-to-uv converts automatically

For a Poetry project with dependencies, a dev group, extras, a private index, markers, and a console script, migrate-to-uv produces a complete PEP 621 pyproject.toml:

Poetry uv
[tool.poetry] metadata [project] metadata (PEP 621)
authors = ["Jane <jane@x.com>"] authors = [{ name = "Jane", email = "jane@x.com" }]
python = "^3.11" requires-python = ">=3.11,<4"
requests = "^2.31" requests>=2.31,<3
httpx = { extras = ["http2"] } httpx[http2]
[tool.poetry.group.dev.dependencies] [dependency-groups] dev
[tool.poetry.extras] [project.optional-dependencies]
[tool.poetry.scripts] [project.scripts]
[[tool.poetry.source]] [[tool.uv.index]] + [tool.uv.sources]
packages = [{ include = "myproj" }] [tool.uv.build-backend] module-name + module-root
Poetry installing every group by default [tool.uv] default-groups = "all"

Caret (^) and tilde (~) constraints become explicit PEP 440 ranges, and dependency markers like sys_platform == 'win32' carry over unchanged.

The default-groups = "all" line preserves Poetry’s behavior but departs from a stock uv project, which defaults to ["dev"]. Leave it if you want uv sync to keep installing everything; drop it if you would rather add a docs group later without it installing unasked.

Check what needs manual cleanup

Three conversions are lossy or surprising. Review each before pushing the change.

  • Build backend. By default the tool replaces poetry-core with uv_build and adds a [tool.uv.build-backend] section. For an application this is fine, but for a package you distribute to PyPIThe Python Package Index, the public repository where Python packages are published and downloaded from. "pip install requests" fetches requests from PyPI. Learn more → , the new backend may include or exclude different files. Pass --keep-current-build-backend to leave [build-system] untouched, or inspect the generated wheelA prebuilt Python package file (.whl) that installs without compiling anything. The standard distribution format for Python packages. Learn more → before publishing.
  • Source priority. Poetry’s priority = "supplemental" and priority = "explicit" on a [[tool.poetry.source]] have no direct uv equivalent. The index URL converts, but you set index precedence yourself in uv.
  • Empty extras. An extra converts only when the package it names is declared in [tool.poetry.dependencies]. Declaration is what matters, not optional = true: a non-optional dependency still converts. When the package is absent from that table entirely, the tool emits cli = [] and warns Could not find dependency "click" listed in "cli" extra. Fix the Poetry file first, or add the dependency to [project.optional-dependencies] by hand.

Map Poetry commands to uv

After migrating, replace poetry invocations with their uv equivalents:

Poetry uv
poetry install uv sync
poetry add requests uv add requests
poetry add --group dev pytest uv add --dev pytest
poetry remove requests uv remove requests
poetry run pytest uv run pytest
poetry env activate source .venv/bin/activate
poetry lock uv lock
poetry update uv lock --upgrade
poetry update requests uv lock --upgrade-package requests
poetry show --tree uv tree
poetry build uv build
poetry publish uv publish

Tip

poetry env activate prints an activation command for you to run; the older poetry shell, which spawned a subshell, moved to a plugin and is no longer installed by default. uv keeps the virtual environmentAn isolated folder where Python installs packages for one project, keeping them separate from other projects and your system Python. Learn more → in .venv at the project root. Activate it with source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows), or prefix commands with uv run instead of activating.

Troubleshooting

Migration aborts on a private index

By default migrate-to-uv locks dependencies at the end, which requires reaching every index. If a private index needs credentials or is unreachable, the lock step fails and the whole migration rolls back:

$ uvx migrate-to-uv
error: Request failed after 3 retries in 4.6s
  Caused by: Failed to fetch: `https://pypi.example.invalid/simple/internal-lib/`
  Caused by: dns error
error: Could not lock dependencies, aborting the migration. Consider using "--ignore-locked-versions" if you don't need to keep versions from the lock file, or "--skip-lock" if you don't want to lock dependencies at all.

uv’s fetch error comes first and names the index that failed. The rollback is complete: pyproject.toml is left byte-identical to what you started with and no uv.lock is written.

The index URL converts, but Poetry’s stored credentials do not. uv reads them from environment variables named after the index. Set them before migrating, where company-repo is the index name uppercased with hyphens replaced by underscores:

$ export UV_INDEX_COMPANY_REPO_USERNAME=<username>
$ export UV_INDEX_COMPANY_REPO_PASSWORD=<password>
$ uvx migrate-to-uv

To convert the metadata first and lock later, run uvx migrate-to-uv --skip-lock, then set the credentials and run uv lock.

An extra converted to an empty list

If migrate-to-uv warns that it could not find a dependency listed in an extra, [tool.poetry.extras] names a package that [tool.poetry.dependencies] never declares. Do not go hunting for a missing optional = true; the line itself is absent. Declare the package in [tool.poetry.dependencies] (conventionally with optional = true) before migrating, or add it directly to [project.optional-dependencies] afterwards:

pyproject.toml
[project.optional-dependencies]
cli = ["click>=8.0,<9"]

A dependency marker looks wrong

Markers convert verbatim, but Poetry’s { markers = "..." } table syntax differs from PEP 508 inline markers. Confirm the migrated line matches what you expect:

pyproject.toml
dependencies = [
    "pywin32>=306,<307 ; sys_platform == 'win32'",
]

Run uv sync on the target platform to confirm the marked dependency installs only where intended.

A console script stopped working

[tool.poetry.scripts] converts to [project.scripts] automatically:

pyproject.toml
[project.scripts]
myproj = "myproj.cli:main"

If the script is missing after uv sync, the project itself was not installed. uv installs the project in editable mode only when a [build-system] is present. Confirm pyproject.toml has one, then re-run uv sync.

Consider poetry-to-uv for read-only conversion

poetry-to-uv prints a converted pyproject.toml to stdout without writing files or generating a lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs. . It covers fewer cases than migrate-to-uv and last released in August 2024, so reach for it only when you want a translation to read rather than apply.

Last updated on