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
6 min read
Sponsored
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 Tool | uv Equivalent |
|---|---|
| pip install | uv add |
| pip install -r requirements.txt | uv sync |
| virtualenv .venv | uv venv (automatic) |
| pyenv install 3.12 | uv python install 3.12 |
| pyenv local 3.12 | uv python pin 3.12 |
| poetry add | uv add |
| pipx install ruff | uv 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
More from this category
More from Technology
Federal Agency Website Funding Restoration: What Digital Teams Should Do Next
R.02 Hackbat: Revolutionizing Embedded Security with Open-Source Hardware
Digital Healthcare Platforms: The $500B Developer Opportunity in 2026
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.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored