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
- uv installed
- A Poetry-managed project with a
pyproject.toml
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.
-
Keep
[project],[dependency-groups], and[build-system]. Retain Poetry’s build settings, includingpackages,include, andexcludeunder[tool.poetry];poetry-corestill uses them. -
Set
[tool.uv].default-groupsto the groups Poetry installs by default. List every non-optional group. For example, ifdevandtestare required butdocsis optional:pyproject.toml[tool.uv] default-groups = ["dev", "test"]This keeps
docsopt-in withuv sync --group docs. If every group is required, usedefault-groups = "all". -
Generate the uv lockfile:
$ uv lockExpect dependency-resolution output and a new
uv.lock. This resolves dependencies afresh and may select different versions frompoetry.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-coreduring the migration. Without--keep-current-build-backend, the converter selectsuv_buildor 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"andpriority = "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, notoptional = true: a non-optional dependency still converts. When the package is absent from that table entirely, the tool emitscli = []and warnsCould 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:
[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:
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:
[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.