Every time I started a new Python project, I went through the same little ritual. I would stare at my terminal. My terminal would stare back. Then I would type python3 -m venv venv, wait for it, remember I needed a specific Python version, Google how to install it, forget pyenv exists, install it with brew, wait for it to compile from source (why), then run pip and watch it slowly download the entire internet into a folder that would one day break for reasons nobody could explain.
Then uv showed up.
The Python packaging ecosystem grew organically over twenty years, which is a diplomatic way of saying it looks like ten different people each solved the same problem in slightly incompatible ways and then went on vacation. You need pip for packages, virtualenv for isolation, pyenv for Python versions, pip-tools for lockfiles, pipx for standalone apps, and a small prayer for good luck.
What Is This Thing
Uv (pronounced "you-vee," not like a piece of fruit) is a Python package and project manager written in Rust. It comes from Astral, the same team that made Ruff, which is the Python linter that made every other linter look like it was running through molasses. Uv wants to replace pip, pip-tools, virtualenv, pipx, and pyenv with a single binary. One file, no dependency chain. No installing a package manager with another package manager so you can install packages.
The Speed Is the Point
I ran a benchmark once. Not a scientific one. I just got tired of waiting for pip to resolve dependencies while my coffee went cold.
Uv without a warm cache is about 8 to 10 times faster than pip. With a warm cache, it can be 80 to 115 times faster. Astral publishes those benchmarks, and you can reproduce them yourself: run the same uv pip install -r requirements.txt command and watch uv finish before pip even finishes printing its first line about collecting metadata.
The speed difference comes from Rust. Pip resolves dependencies in the same language your dependencies are written in. That circularity is neat in theory and miserable in practice.
"The performance speaks for itself" is the sort of thing people write when they have nothing else to say, but in this case the performance actually does speak for itself. You run uv add requests and it is done before you finish typing the command.
The Great Replacement
Here is what uv replaces:
| The Old Way | The Uv Way |
|---|---|
| pip install | uv pip install or uv add |
| pip freeze | uv pip freeze |
| python3 -m venv venv | uv venv |
| pip install pip-tools | Built into uv add and uv lock |
| pyenv install 3.12 | uv python install 3.12 |
| pipx install | uv tool install |
| requirements.txt + constraints.txt | uv.lock |
The pip compatibility mode makes this easy. If you already have a project with requirements.txt and a Makefile and seventeen shell aliases, you can just start typing uv pip install instead of pip install and the day to day syntax is the same. Astral calls it a drop-in replacement, not an exact clone, and the gaps are documented: uv ignores pip.conf and PIP_INDEX_URL in favour of its own config, will not install into your system Python without --system, and skips bytecode compilation unless you pass --compile-bytecode. None of that broke my workflow. It is just ten times faster.
You can migrate gradually. Nobody is demanding you rewrite your whole project structure on day one.
Installing It
The install command is about as simple as it gets:
curl -LsSf https://astral.sh/uv/install.sh | sh
No Python required and no Rust toolchain. It downloads a single binary and puts it somewhere on your PATH.
If you already have Python and pip installed (you probably do), you can also do:
pip install uv
Or if you use pipx:
pipx install uv
Personally I like the curl method because it feels like I am installing something that does not depend on the thing it is replacing. There is something uncomfortable about using pip to install a tool that is supposed to replace pip. It is like using a horse to transport the car you just bought.
The Command Tour
Once uv is installed, the way you use it depends on what stage of a project you are at.
Starting Something New
uv init my-project
cd my-project
uv add requests
uv run python -c "import requests; print('it works')"
uv init creates a pyproject.toml, a .gitignore, and the basic project skeleton. uv add downloads the package, resolves its dependencies, writes a lockfile, and makes it available. uv run activates the virtual environment and runs your command in one step, so there is no separate activation and nothing to source by hand.
Working With an Existing Project
git clone https://github.com/someone/their-project
cd their-project
uv sync
uv sync reads the project's pyproject.toml and uv.lock, creates a virtual environment if one does not exist, installs everything, and produces or updates a uv.lock file for reproducibility. One command. Done. (For a project that only has a requirements.txt, use uv pip sync requirements.txt, or migrate first with uv add -r requirements.txt.)
Running a Single Script
uv run script.py
Uv automatically creates an isolated environment for the script. You can even declare dependencies inline at the top of the file, and uv will pick them up without needing a separate requirements file. This is handy for the kind of ad hoc script you write at 2am to parse a CSV and never look at again.
Managing Python Versions
This is where uv really earns its keep.
uv python install 3.12 # installs the latest 3.12.x
uv python install 3.9 3.10 3.11 # installs multiple versions
uv python list # shows what you have installed
uv python pin 3.12 # sets the project Python version
Uv downloads prebuilt Python binaries. It does not compile from source. This means installing Python 3.12 takes seconds instead of the twenty minutes it takes pyenv to compile it on a laptop that is already running a browser with forty tabs open.
How Uv Finds Python on Your Machine
Uv finds Python interpreters through a multi-step search. It first checks its own managed installations (the versions it downloaded itself). Then it walks your system PATH looking for executables named python, python3, or python3.x. On Windows it checks the registry and the Microsoft Store. On any machine it checks currently active virtual environments. It queries each candidate for version metadata and picks the one that matches your constraints.
If nothing matches, it downloads the version you need automatically. Zero manual intervention.
This sounds minor on paper but it saves you roughly once per project. You know how many times I have typed python3.12 only to get "command not found"? Enough times that automatic fallback feels like a luxury feature.
The Shell Integration
I almost never use this, but it exists. You can add a line to your .zshrc or .bashrc that aliases pip to uv pip and automatically activates a default virtual environment. It makes the transition even smoother if you are the kind of person who does not want to learn new muscle memory.
I have not done it because I enjoy typing uv and feeling cool, but it is there if you want it.
Upgrade Path
uv self update
That is it. One command. It downloads the latest version. No pip install --upgrade, no brew upgrade, no manual download from GitHub releases.
Should You Switch
I switched about six months ago and I have not touched pip directly since (except on servers where uv is not installed, which I am slowly fixing one at a time). The virtual environment management is automatic, dependency resolution is fast, Python version management actually works, and I no longer maintain half a dozen separate package management tools.
The main downside is that uv is relatively young. Most of the bugs I have hit were edge cases with exotic dependency graphs, and Astral has been responsive about fixing them. The ecosystem around uv is still growing. Certain niche workflows (conda interop, namespace packages with specific directory structures) can be bumpy.
For normal Python development - web apps, data scripts, CLI tools, API services - uv works great. It is faster and simpler than what came before.
I wish I had switched sooner.