# How to migrate from Poetry to uv


[Poetry](https://pydevtools.com/handbook/reference/poetry.md) and [uv](https://pydevtools.com/handbook/reference/uv.md) both manage dependencies and virtual environments from a single tool, but uv adds Python version management, faster resolution, and a standards-first {{< term "pyproject-toml" "pyproject.toml" >}} layout. Migrating rewrites Poetry's `[tool.poetry]` tables into [PEP 621 project metadata](https://pydevtools.com/handbook/explanation/what-is-pep-621-compatibility.md), 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

- [uv installed](https://pydevtools.com/handbook/how-to/how-to-install-uv.md)
- A [Poetry](https://pydevtools.com/handbook/reference/poetry.md)-managed project with a `pyproject.toml`

## Convert the project with migrate-to-uv

[`migrate-to-uv`](https://osprey-oss.github.io/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

```console
$ 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

```console
$ 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

```console
$ 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](https://pydevtools.com/handbook/explanation/versioning-python-packages-semver-calver-and-pep-440.md), and [dependency markers](https://pydevtools.com/handbook/reference/pyproject.toml.md) 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](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md).** 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 {{< term "pypi" "PyPI" >}}, the new backend may include or exclude different files. Pass `--keep-current-build-backend` to leave `[build-system]` untouched, or inspect the generated {{< term "wheel" >}} 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](https://pydevtools.com/handbook/reference/uv.md) 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 {{< term "virtual-environment" "virtual environment" >}} 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:

```console
$ 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:

```console
$ 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:

```toml {filename="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](https://pydevtools.com/handbook/explanation/what-is-pep-508.md). Confirm the migrated line matches what you expect:

```toml {filename="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:

```toml {filename="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`](https://pypi.org/project/poetry-to-uv/) prints a converted `pyproject.toml` to stdout without writing files or generating a {{< term "lockfile" >}}. 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.
