Skip to content

Technology · Developer Tooling

uv: How We Replaced pip, poetry, and pyenv With One Tool

uv is a Python package manager written in Rust that handles dependencies, virtual environments, and Python version management. We've been using it across all our projects since early 2026. Here's what actually changed.

Anurag Verma

Anurag Verma

6 min read

uv: How We Replaced pip, poetry, and pyenv With One Tool

Sponsored

Share

For years, setting up a Python project meant choosing between several reasonable but painful options: pip with a requirements.txt, virtualenv managed by hand, poetry with its own file format, pyenv for Python version switching, and pipx for CLI tools. Each solved a piece of the problem. None solved all of it.

uv solves all of it. It’s a Python package manager written in Rust by the team at Astral (who also make Ruff, the Python linter). It installs packages 10-100x faster than pip, manages virtual environments, handles Python version installation, and produces lockfiles that work across platforms.

We moved all our Python projects to uv in January 2026. This is what the transition looked like.

What uv Actually Does

The core functions, in order of what you’ll use most:

Package installation: uv add installs packages and updates your pyproject.toml. uv sync installs everything in the lockfile.

Virtual environment management: uv creates and manages .venv automatically. You don’t have to activate it manually for most commands.

Python version management: uv python install 3.12 downloads and manages Python versions — no pyenv needed.

Lockfile generation: uv lock produces a uv.lock file that pins every dependency (including transitive ones) with hashes, reproducible across machines.

Script running: uv run python script.py runs a script in the project’s virtual environment without activation.

Tool installation: uv tool install ruff installs CLI tools in isolated environments — replaces pipx.

Installation

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

# Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# Or via pip if you prefer
pip install uv

After installation, uv is a single binary. No dependencies. No Python required.

Starting a New Project

uv init my-project
cd my-project

This creates:

my-project/
├── pyproject.toml
├── .python-version
├── README.md
└── hello.py

The pyproject.toml is minimal by default:

[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

Add dependencies:

uv add fastapi uvicorn
uv add --dev pytest ruff mypy

This updates pyproject.toml and generates uv.lock. The lockfile is deterministic — commit it to your repo.

The Virtual Environment Situation

uv creates a .venv in your project directory automatically. You can activate it the usual way, but you don’t have to:

# Runs in the project's venv without activation
uv run python -m pytest

# Or activate manually
source .venv/bin/activate

In CI and Docker, uv run is cleaner than managing activation. In local development, activating the venv lets you use bare python and pytest commands as you normally would.

Python Version Management

Replace pyenv with uv:

# Install a specific Python version
uv python install 3.12.3

# Set the project's Python version
uv python pin 3.12.3

# Install and use immediately
uv run --python 3.12 python --version

The .python-version file (same format as pyenv) tells uv which version to use for the project. Anyone cloning the repo runs uv sync and gets the right Python version installed automatically.

Migrating From pip + requirements.txt

If you have an existing project:

# Convert requirements.txt to pyproject.toml + uv.lock
uv init --no-readme  # In your existing project
uv add $(cat requirements.txt | grep -v '^#' | tr '\n' ' ')
uv lock

For development requirements:

uv add --dev $(cat requirements-dev.txt | grep -v '^#' | tr '\n' ' ')

Then delete requirements.txt. Your pyproject.toml and uv.lock replace it.

Migrating From poetry

If you use pyproject.toml with [tool.poetry.dependencies]:

# uv can read poetry's format during import
uv import pyproject.toml  # Not a real command — do this manually

# Manual approach: run this in the poetry project
poetry export --without-hashes > requirements-temp.txt
uv add $(cat requirements-temp.txt | cut -d';' -f1 | tr '\n' ' ')
rm requirements-temp.txt

The manual approach takes 10 minutes for most projects. The bigger gain is dropping poetry.lock for uv.lock, which tends to resolve faster and fail less in CI.

CI Integration

A typical GitHub Actions workflow:

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install uv
        uses: astral-sh/setup-uv@v4
        with:
          version: "latest"
      
      - name: Set up Python
        run: uv python install
      
      - name: Install dependencies
        run: uv sync --all-extras --dev
      
      - name: Run tests
        run: uv run pytest
      
      - name: Lint
        run: uv run ruff check .

The setup-uv action caches the uv binary and your virtual environment. On a warm cache, uv sync on a 40-package project takes under a second. With pip + virtualenv, the same step takes 30-60 seconds.

Docker

FROM python:3.12-slim

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app

# Copy dependency files first (layer caching)
COPY pyproject.toml uv.lock ./

# Install dependencies
RUN uv sync --frozen --no-dev

# Copy application
COPY . .

CMD ["uv", "run", "python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0"]

--frozen ensures the lockfile is used exactly as-is without resolution. If uv.lock doesn’t match pyproject.toml, the build fails — good behavior for reproducible containers.

What We Actually Noticed

After 4 months using uv across 8 projects:

CI time: Down significantly. The biggest factor isn’t just uv’s speed — it’s the lockfile caching. With requirements.txt and pip, you often can’t cache reliably across runs because the resolved set changes. With uv.lock, the cache hit rate is near 100%.

Dependency conflicts: We had fewer resolution errors. uv’s resolver is stricter about conflicts, which means problems surface immediately instead of silently producing broken environments.

Onboarding: New developers run uv sync and get a working environment. No documentation needed for “first, install pyenv, then set Python version, then create virtualenv, then install dependencies.” One command.

One thing that broke: A few packages we used had non-standard build systems that pip handled with fallbacks that uv doesn’t. Rare, but if you have exotic C extension packages, test them before committing to the migration.

The Tool Replacement Summary

Old Tooluv Equivalent
pip installuv add
pip install -r requirements.txtuv sync
virtualenv .venvuv venv (automatic)
pyenv install 3.12uv python install 3.12
pyenv local 3.12uv python pin 3.12
poetry adduv add
pipx install ruffuv tool install ruff

The migration is low-risk. uv reads standard pyproject.toml and produces a standard lockfile format. If you decide you hate it, you can generate a requirements.txt from the lockfile with uv export.

Most teams that try it don’t go back.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored