Skip to content

How to migrate from Poetry to uv

Poetry and uv both manage dependencies and virtual environments and support standard pyproject.tomlThe standard configuration file for Python projects. Declares the project name, version, dependencies, build system, and tool settings in one place. Learn more → metadata. Poetry also has experimental Python interpreter management. See How do uv and Poetry compare? before deciding to switch.

Migration converts legacy [tool.poetry] metadata to PEP 621 project metadata, replaces poetry.lock with uv.lock, and updates tool-specific settings. Existing [project] metadata can stay in place.

Prerequisites

Convert the project with migrate-to-uv

Warning

Check pyproject.toml for [dependency-groups] before running the converter. For standard groups, follow the manual migration steps. The converter can reject valid Poetry group settings or overwrite standard groups. Adding an empty legacy group dependency table does not safely work around this limitation.

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 --keep-current-build-backend

--dry-run prints the converted pyproject.toml without touching any files. --keep-current-build-backend preserves [build-system] and Poetry’s package-discovery settings. Read the output before committing to the migration.

2. Run the migration

$ uvx migrate-to-uv --keep-current-build-backend

Expect Successfully migrated project from Poetry to uv! after the dependency-resolution output. The tool rewrites pyproject.toml, generates uv.lock from the versions already pinned in poetry.lock, and removes the Poetry-specific files. The build backend stays in place.

Migrate standard metadata and groups manually

Use this path when metadata and dependencies already live in [project] and [dependency-groups]. If any dependency declarations remain in legacy Poetry tables, move them into standard tables first, preserving their constraints.

For Poetry-specific package sources, configure uv indexes and sources before proceeding. Retain build plugins required by poetry-core in [build-system].requires; replace any Poetry command plugins before updating scripts or CI.

  1. Keep [project], [dependency-groups], and [build-system]. Retain Poetry’s build settings, including packages, include, and exclude under [tool.poetry]; poetry-core still uses them.

  2. Set [tool.uv].default-groups to the groups Poetry installs by default. List every non-optional group. For example, if dev and test are required but docs is optional:

    pyproject.toml
    [tool.uv]
    default-groups = ["dev", "test"]

    This keeps docs opt-in with uv sync --group docs. If every group is required, use default-groups = "all".

  3. Generate the uv lockfile:

    $ uv lock
    

    Expect dependency-resolution output and a new uv.lock. This resolves dependencies afresh and may select different versions from poetry.lock. Check the selected versions before removing the old lockfile.

Verify the result

Run the project’s test suite after syncing. For a project that uses pytest:

$ uv sync
$ uv run pytest

uv sync installs the locked dependencies into .venv. uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first. Expect the test suite to pass before replacing Poetry commands in scripts and CI.

For a distributed package, also check that the retained backend builds successfully:

$ uv build

Expect a wheel and a source distribution in dist/. After verification succeeds, remove poetry.lock if it remains.

Convert metadata and dependency settings

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, and exclude Retained under [tool.poetry] with --keep-current-build-backend
Poetry installing all non-optional groups by default [tool.uv] default-groups preserves the selected groups

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

Poetry installs all non-optional dependency groups by default; a stock uv project installs only dev. For legacy Poetry groups, migrate-to-uv writes default-groups = "all" when none are optional, or an explicit list of non-optional groups otherwise. Review that setting before syncing so optional tools stay opt-in.

Check what needs manual cleanup

Review these settings before pushing the migration.

  • Build backend. Keep poetry-core during the migration. Without --keep-current-build-backend, the converter selects uv_build or Hatchling, which may change package discovery or included files. Switch backends separately, comparing the contents of the wheelA prebuilt Python package file (.whl) that installs without compiling anything. The standard distribution format for Python packages. Learn more → and source distribution 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; poetry shell requires the separate poetry-plugin-shell plugin. 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.

Resolve migration errors

Restore access to 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 --keep-current-build-backend
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 --keep-current-build-backend

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

Restore a missing extra dependency

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"]

Check a converted dependency marker

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.

Restore a console script

[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. . For projects using legacy Poetry groups, use uvx migrate-to-uv --dry-run --keep-current-build-backend to preview the same converter that will perform the migration.

Last updated on