Migrate from Poetry to uv and Speed Up Your Builds 10x

Learn how to migrate from poetry to uv safely while preserving exact dependencies and speed up your Python 3.14 build pipelines by up to 10x.

Migrate from Poetry to uv and Speed Up Your Builds 10x
Source (Personal archive/maiastudios.com.br)

The Python packaging ecosystem has undergone massive changes over the past year. For a long time, Poetry stood out as the standard tool for dependency management, virtual environment handling, and package publishing. However, the arrival of uv dramatically raised the bar for workflow speed. Written in Rust, uv replaces tools like pip, pip-tools, virtualenv, and Poetry itself with an impressive performance boost. Choosing to migrate from poetry to uv isn't just about chasing shiny new trends—it's a direct productivity upgrade that cuts CI/CD pipeline execution times from minutes down to a few seconds on Python 3.14.7.

In this article, I'll walk you through a practical, step-by-step process to handle this transition smoothly, preserving your project's exact dependency graph and preventing production outages.

Why Switching from Poetry to Modern Tools Has Become Essential

Poetry relies on its own custom dependency resolver written in pure Python. While it ensures consistency, its resolution algorithm (a SAT solver) often runs into severe bottlenecks when handling deep dependency trees or heavy packages. In enterprise projects, simply adding a new library can take dozens of seconds—or even minutes—just to recalculate the tree and update the lockfile.

uv solves this by rebuilding the entire resolution and downloading engine from scratch. It features an extremely fast, concurrent dependency resolver written in Rust that downloads and builds wheels in a fraction of the time traditional tools require. Beyond speed, uv adopts standard Python Packaging Authority (PyPA) specifications by default—specifically PEP 621 for defining project metadata inside pyproject.toml.

Because Poetry pre-dates the widespread adoption of PEP 621, it uses a proprietary [tool.poetry] section that isolates project configuration from modern standards. Migrating aligns your codebase with universal community specifications, eliminating lock-in to a single tool.

How to Migrate from Poetry to uv in Your Project Without Breaking Dependencies

Vector illustration on a dark background comparing two dependency resolution workflows: one long and winding, and another fast and direct with cyan and violet arrows.
Source (Personal archive/maiastudios.com.br)

The biggest fear when switching package managers is accidentally bumping a transitive library version in production. The conversion process must preserve your application's current state by migrating top-level declarations and regenerating the lockfile with equivalent guarantees.

Step 1: Convert the Dependency Section to PEP 621 Standard

In Poetry, dependencies live under [tool.poetry.dependencies] and [tool.poetry.group.dev.dependencies]. Under PEP 621 (used by uv), production dependencies belong in project.dependencies, while development groups go into dependency-groups (per PEP 735) or project.optional-dependencies.

You can perform this migration manually or use uv's built-in automation commands to inspect your current setup. To migrate the structure in your pyproject.toml directly, update the key layout.

Legacy Poetry structure:

[tool.poetry]
name = "meu-sistema"
version = "0.1.0"
description = "API de processamento de dados"
authors = ["Dev Team <dev@empresa.com>"]

[tool.poetry.dependencies]
python = "^3.14"
fastapi = "^0.115.0"
uvicorn = {extras = ["standard"], version = "^0.30.0"}

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
ruff = "^0.6.0"

Converted structure for the PEP 621 standard accepted by uv:

[project]
name = "meu-sistema"
version = "0.1.0"
description = "API de processamento de dados"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
    "fastapi>=0.115.0",
    "uvicorn[standard]>=0.30.0",
]

[dependency-groups]
dev = [
    "pytest>=8.0.0",
    "ruff>=0.6.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Note that we replaced the poetry-core build backend with hatchling (or flit_core), which are modern, lightweight build backends supported across the standard ecosystem.

Step 2: Export the Old Lockfile to Ensure Integrity

To make sure no sub-dependency changes version during your first sync with uv, the safest strategy is to export Poetry's exact tree to a temporary pip requirements file before generating the new uv.lock.

Run the export command in Poetry:

poetry export -f requirements.txt --output requirements-temp.txt --with dev

Next, use uv to compile a fresh lockfile based strictly on those pinned versions, ensuring the initial resolution matches your existing poetry.lock:

uv pip compile requirements-temp.txt -o requirements-resolved.txt

Now you can generate the native uv.lock file by initializing the project with uv:

uv lock

Once you confirm uv.lock was created successfully, you can safely remove poetry.lock and requirements-temp.txt from your repository.

Step 3: Create the Virtual Environment and Sync

With your new configuration ready, remove the old virtual environment folder managed by Poetry (usually located at ~/.cache/pypoetry/virtualenvs or in the root as .venv) and let uv spin up a new isolated environment:

# Remove the old environment if located at the root
rm -rf .venv

# Create a new virtual environment bound to Python 3.14.7
uv venv

# Sync the environment with dependencies from uv.lock
uv sync

The uv sync command performs an atomic, lightning-fast installation of all packages, leaving your .venv ready for immediate use.

How to Tweak Your CI/CD Pipeline for the New Package Manager

One of the biggest returns on investment when switching to uv comes in automated CI/CD pipelines. The time spent setting up Python environments on cloud runners drops drastically thanks to uv's global caching system and zero build overhead.

In a standard GitHub Actions workflow, the installation step that previously required installing Python, setting up Poetry via pip, and running poetry install can be simplified using the official astral-sh/setup-uv action.

Here is a side-by-side example of a workflow configured to test and validate code with uv:

name: Pipeline de Integração Contínua

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Instalar uv
        uses: astral-sh/setup-uv@v3
        with:
          version: "latest"
          enable-cache: true

      - name: Configurar Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.14"

      - name: Sincronizar Dependências
        run: uv sync --all-groups

      - name: Executar Testes
        run: uv run pytest

The enable-cache: true flag instructs the action to cache uv's package layers. Because wheel downloads are executed concurrently in Rust, dependency installation time on the runner routinely drops from 45 seconds to under 3 seconds.

In Docker containerization, performance gains are equally noticeable. Here is how to structure a production-oriented Dockerfile using multi-stage builds:

FROM python:3.14-slim AS builder

# Copy the uv executable directly from the official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

WORKDIR /app

# Copy only definition files to optimize layer caching
COPY pyproject.toml uv.lock ./

# Sync dependencies without installing the current project or dev packages
RUN uv sync --frozen --no-dev --no-install-project

FROM python:3.14-slim

WORKDIR /app

# Copy the compiled virtual environment from the builder stage
COPY --from=builder /app/.venv /app/.venv
COPY .

ENV PATH="/app/.venv/bin:$PATH"

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

The --frozen flag ensures uv won't attempt to update uv.lock during container build, failing fast if your lockfile is out of sync with pyproject.toml.

Common Pitfalls When Switching Package Managers and How to Fix Them

Close-up photo of a mechanical keyboard illuminated on a dark desk with a blurred ultrawide monitor emitting cyan light in the background.
Source (Personal archive/maiastudios.com.br)

While the migration process is straightforward, there are configuration edge cases that can trigger errors if not addressed beforehand.

Handling Private Repositories and Custom Indexes

If your project relies on a private PyPI index (like Nexus, Artifactory, or AWS CodeArtifact), Poetry required configuration via poetry config repositories. In uv, private indexes are defined directly in pyproject.toml using [[tool.uv.index]] tables or environment variables.

Example declaration in pyproject.toml:

[[tool.uv.index]]
name = "repositorio-interno"
url = "https://pypi.empresa.com/simple/"
explicit = true

By setting explicit = true, you prevent uv from checking public PyPI for packages registered under this repository, protecting your builds against dependency confusion attacks.

Managing Command-Line Scripts

In Poetry, CLI entry points were defined under [tool.poetry.scripts]. Standardized under PEP 621, these scripts now belong under [project.scripts].

# Old Poetry format
# [tool.poetry.scripts]
# meu-cli = "meu_pacote.cli:main"

# PEP 621 standard format for uv
[project.scripts]
meu-cli = "meu_pacote.cli:main"

When running uv pip install -e . or uv sync, the executable meu-cli is installed into .venv/bin and can be run via uv run meu-cli.

Differences in Editable Package Resolution

Monorepos or projects using local editable dependencies (linked via -e) need extra attention. Poetry declared local path dependencies using { path = "../outro-pacote", develop = true }. In uv, the syntax follows uv source declarations:

[tool.uv.sources]
outro-pacote = { path = "../outro-pacote", editable = true }

Decoupling the minimum version constraint in project.dependencies from the local path source in tool.uv.sources keeps your pyproject.toml fully standards-compliant without vendor lock-in.

Feature Comparison Table and Verdict

To visualize what changes in your daily workflow after replacing Poetry with uv, review this breakdown of key operational aspects:

Feature Poetry uv (Astral)
Engine Language Pure Python Rust
Metadata Standard Proprietary [tool.poetry] section Official PEP 621 ([project])
Lockfile poetry.lock uv.lock
Average Lock Time 10s to 120s on medium projects Under 1s in almost all scenarios
Python Version Management Requires pyenv or pre-installed Python Installs and manages Python versions natively (uv python install)
Isolated Tool Execution Requires prior plugin installation Runs tools on-demand like npx (uvx ruff check)

The technical verdict is clear: Poetry played a crucial historical role by introducing lockfiles and modern project management to Python. However, uv's architecture provides a technical edge that is hard to ignore. Combining extreme performance in Rust with strict adherence to PyPA standards makes uv the top choice for new projects and a high-priority upgrade for existing codebases.

Conclusion

Choosing to migrate from poetry to uv is one of the highest-ROI infrastructure upgrades a Python team can make. By embracing PEP 621 standards and leveraging Rust-powered speed, you eliminate CI/CD pipeline bottlenecks, simplify Python toolchain management, and reduce build complexity. The migration path is safe, fully reversible, and guarantees the exact dependency parity required for production stability.

Enjoyed it? Share

More in Python & Code