Table of Contents

So, here I am staring at my terminal, watching Poetry hang on “Resolving dependencies…” for the tenth time today. Don’t get me wrong—Poetry was a massive leap forward for Python packaging. It saved us from the dark ages of `requirements.txt` chaos. But as my projects scaled and my CI/CD pipelines got heavier, the slow resolution times started to add up. That’s when I finally decided it was time to bite the bullet and figure out the process of migrating Poetry to UV package manager.

If you haven’t heard of it, uv is an extremely fast Python package and project manager written in Rust by the team at Astral (the same folks behind Ruff). It acts as a drop-in replacement for `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, and `virtualenv`. The speed difference isn’t just noticeable—it’s staggering. We’re talking resolving dependencies in milliseconds instead of minutes.

Let’s figure this out together. I’m going to walk you through exactly how I migrated my production projects from Poetry to UV, avoiding the outdated tools and focusing on the modern, standard approaches.

Prerequisites: Getting UV Installed

Before we touch our `pyproject.toml` files, we need UV installed globally on our system. Forget about `pip install uv-pm` (that’s an outdated wrapper). You want the official binary.

# On macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# On Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Once installed, you can verify it by running `uv –version`. You should see something like `uv 0.4.x` or newer.

Method 1: The Automated Way (migrate-to-uv)

If you have a fairly standard Poetry setup without overly complex private indexes or custom plugins, the community-maintained `migrate-to-uv` tool is absolutely brilliant. It parses your `pyproject.toml` and automatically rewrites the `[tool.poetry]` blocks into standard PEP 621 `[project]` blocks.

You don’t even need to install it permanently; you can just run it using UV’s temporary execution tool, `uvx`.

# Navigate to your project directory
cd my-python-project

# Run the migration script
uvx migrate-to-uv

This script does three major things:

  1. It converts your Poetry dependencies into standard `dependencies` arrays.
  2. It preserves your Poetry dependency groups (like `dev` or `test`) by moving them to the `[dependency-groups]` section.
  3. It cleans up the old Poetry boilerplate.

Method 2: The Manual Native Way (My Preference)

Why did I decide to build it this way? Because relying on third-party conversion scripts can sometimes mess up intricate configurations. I strongly prefer doing it manually to ensure my `pyproject.toml` is perfectly aligned with standard Python packaging standards.

Step 1: Update the Build System

Poetry relies on `poetry-core` as its build backend. UV prefers standard backends like `hatchling`. Open your `pyproject.toml` and change the `[build-system]` section:

# Before (Poetry)
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

# After (Standard/UV)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Step 2: Convert to PEP 621 Standard

Poetry uses its proprietary `[tool.poetry]` tables. You need to convert this to the standard `[project]` table.

# Remove this:
[tool.poetry]
name = "my-app"
version = "0.1.0"

[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.100.0"

# Replace with this:
[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "fastapi>=0.100.0",
]
Notice the syntax change! Poetry uses caret (`^`) notation which isn’t strictly standard in PEP 621. You’ll need to convert those to standard greater-than-or-equal (`>=`) or exact (`==`) constraints.

Step 3: Handling Dev Dependencies

In Poetry, you might have `[tool.poetry.group.dev.dependencies]`. In UV, this simply becomes standard dependency groups:

[dependency-groups]
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
]

Generating the UV Lockfile

Once your `pyproject.toml` is cleaned up, it’s time to generate the new lockfile. First, completely delete your old Poetry files.

rm poetry.lock
rm -rf .venv

Now, let UV work its magic. Run the sync command. This will resolve dependencies, generate a fresh `uv.lock` file, create a virtual environment (`.venv`), and install all packages in milliseconds.

uv sync

If you want to add a new package later, instead of `poetry add requests`, you simply run:

uv add requests

Updating CI/CD Workflows (GitHub Actions)

This is arguably the best part of migrating Poetry to UV package manager. Your CI/CD pipelines will speed up dramatically. You no longer need heavy Python setup steps caching massive `.venv` folders.

Here is what a modern GitHub Actions workflow looks like with UV:

name: CI

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install uv
        uses: astral-sh/setup-uv@v5

      - name: Set up Python
        run: uv python install

      - name: Install dependencies
        run: uv sync --all-groups

      - name: Run tests
        run: uv run pytest

Notice the command replacement: wherever you previously used `poetry run`, you now use `uv run`. It behaves exactly the same way, executing the command inside the isolated virtual environment.

Final Thoughts

Migrating from Poetry to UV was one of the highest ROI technical decisions I’ve made this year. It eliminated the dreaded dependency resolution hangs, sped up my CI/CD builds by over 60%, and allowed my team to drop non-standard `tool.poetry` configurations in favor of official PEP standards.

If you have a spare afternoon, create a new branch and test it out. Run `uvx migrate-to-uv`, type `uv sync`, and watch how fast your environment springs to life. You won’t look back.

Categorized in: