It started on a Tuesday morning. I was trying to spin up a quick Python script to parse some local JSON logs. Standard, routine developer stuff.
But first, I needed Python 3.12. I ran pyenv install 3.12.2.
Five minutes passed. My laptop fan started whirring as it compiled Python from source, eating up my CPU and my patience.
Then, I realized I needed a global tool—let’s say ruff for linting. I ran pipx install ruff. But pipx was bound to my older global Python 3.11 installation.
So I had to update pipx‘s default python interpreter.
Finally, I went into my project folder, ran virtualenv .venv, activated it, and ran pip install requests.
By the time I actually wrote import requests, twenty minutes of my life had completely vanished.
I wasn’t writing code. I was acting as an amateur system administrator for a disjointed, fragile stack of local Python toolings.
For years, the Python ecosystem told us this was the “correct” and “standard” way.
We accepted the Holy Trinity: pyenv to manage python versions, pipx to manage global developer CLI utilities, and virtualenv (or Poetry/Pipenv) to manage local virtual environments.
But then Astral released uv.
And my entire mental model of what Python development should feel like completely shattered.
The Core Problem: The Brittle Multi-Tool Stack
Let’s be honest. The old way isn’t just slow; it’s mentally exhausting.
Each tool in the traditional stack operates in its own isolated bubble, unaware of the others.
pyenv relies heavily on shell shims. If you update your shell profile incorrectly, or if you switch shells, your paths break, and suddenly python points back to your system’s default installation.
pipx works well, but it is slow and heavy. It creates a dedicated virtualenv and copies symlinks to your global path every single time you want to run a simple CLI helper.
And virtualenv requires manual activation, shell adjustments, and constant cleanup of orphan directories that eat up gigabytes of local storage.
When you onboard a new developer to a team, you don’t just hand them a repository.
You hand them a multi-step installation guide: “Install Homebrew, then run pyenv, then configure your shell paths, then install pipx, then install poetry, then run poetry install…”
If a single step fails due to local configuration drift, you spend the next hour debugging paths. It is a fragile house of cards.
The One Killer Feature: Unified Toolchain Orchestration
uv is famous for being incredibly fast because it is written in Rust. But speed is just a side-effect.
The real game-changing feature is Unified Toolchain Orchestration.
uv replaces all of these disparate tools with a single, self-contained, statically linked binary.
It doesn’t rely on your system’s pre-installed Python. It doesn’t need C compiler tools installed on your host to build Python from source. It doesn’t pollute your shell with fragile path shims.
When you run a command in uv, it looks at your workspace, downloads the exact Python version required as a pre-built binary in milliseconds, builds an isolated virtualenv, and runs your script.
Everything is consolidated, predictable, and incredibly clean.
How uv Replaces the Traditional Stack
Let’s look at exactly how uv makes pyenv, pipx, and virtualenv completely obsolete with clean, unified commands.
1. R.I.P. pyenv → Hello “uv python”
With pyenv, installing a new Python version is an exercise in patience.
With uv, Python versions are treated as managed toolchains.
It downloads pre-compiled, highly optimized builds from the Greg-Heo/indygreg python-build-standalone repository in literally 2-3 seconds.
# List all available Python versions you can install
uv python list
# Install a specific version instantly
uv python install 3.12
# Pin a local directory to a specific version
uv python pin 3.12.2
2. R.I.P. pipx → Hello “uv tool”
pipx was great for installing tools like black, ruff, or ansible in isolated global scopes.
But uv tool does the same thing, with near-zero latency.
It manages global executable packages in independent, ephemeral environments without manual path management.
# Install a tool globally
uv tool install ruff
# Run a tool on-the-fly without permanently installing it
uvx ruff format .
# (or: uv tool run ruff format .)
3. R.I.P. virtualenv → Hello “uv venv” & “uv run”
Creating and managing virtual environments is now instant.
But the real quality-of-life upgrade is uv run.
You no longer have to run source .venv/bin/activate or manage shell states.
uv run automatically detects the local .venv and executes the command within that context, completely isolating execution.
# Create a virtualenv instantly (takes around 10ms)
uv venv
# Install dependencies into it (using high-speed hardlinks)
uv pip install requests
# Run a script directly inside the virtualenv without activating it
uv run script.py
Side-by-Side Command Comparison
Here is how the common workflow actions compare side-by-side between the multi-tool legacy stack and the unified uv stack. These tab components show proper multi-line references:
Command reference for the multi-tool stack:
- 1. Install Python:
pyenv install 3.12.2 - 2. Create virtualenv:
python -m venv .venv && source .venv/bin/activate - 3. Install global CLI tools:
pipx install ruff - 4. Run temporary script tool:
pipx run ruff format .
Command reference for the single uv binary:
- 1. Install Python:
uv python install 3.12.2 - 2. Create virtualenv:
uv venv && uv run script.py - 3. Install global CLI tools:
uv tool install ruff - 4. Run temporary script tool:
uvx ruff format .
The Measured Wins: Old vs New
Let’s look at the actual numbers.
Here is what I measured on my local machine when comparing the bootstrap and build times for a fresh developer workspace:
| Metric / Action | The Legacy Stack | The uv Stack | Performance Delta |
|---|---|---|---|
| Install Python 3.12 | 5m 12s (Compile from source) | 3.1s (Pre-built standalone binary) | ~100x Faster |
| Create Virtualenv | 1.2s (Standard venv module) | 0.015s (uv venv) | ~80x Faster |
| Install 10 dependencies | 14.8s (Standard pip resolver) | 0.45s (uv pip install with global cache) | ~32x Faster |
| Global CLI execution (uvx) | 1.8s (pipx resolution bootstrap) | 0.08s (uvx execution) | Instantaneous |
| Local Shell Footprint | Complex path shims & env hooks | Zero (Single static binary) | Clean Shell |
The Honest Gotchas (Be Aware)
No tool is perfect. When you adopt uv as your primary toolchain manager, there are a few important nuances to keep in mind:
- Standalone Python Locations:
uvstores its downloaded Python versions in its own managed cache folder (usually~/.local/share/uv/pythonon Unix or%LOCALAPPDATA%\uv\uv\pythonon Windows). If you have other developer tools, IDEs, or legacy scripts that hardcode path searches forpyenv‘s versions directory, you will need to point them to uv’s toolchain directory instead. - Unified Lockfile Coexistence: If you are moving to a full
uvworkspace, all project packages must agree on shared dependency constraints. While this is an excellent best practice for monorepos, it can require some initial coordination and version upgrades across older, loosely coupled internal libraries. - Extremely Fast Caching: Because
uvheavily utilizes global content-addressable storage (caches) and hardlinks, deleting a local.venvfolder won’t actually free up disk space until you runuv cache cleanto prune the global store.
Closing Thoughts
Swapping out my disjointed multi-tool stack for uv wasn’t just about shaving off a few seconds from my package installation time.
It was about reclaiming mental focus.
I stopped fighting my local configurations, stopped debugging shell path shims, and finally made my Python environments incredibly boring—the absolute highest praise you can give to software infrastructure.
