
For years, a slow package install was just background noise. On small projects it didn’t matter: a few packages, a few seconds, done. But on anything with a real requirements file, pip could sit there for a minute or two just resolving and downloading. Long enough to context-switch into something else and come back when it was done. That was just the cost of the thing.
Then a dependency conflict I’d been ignoring for months finally bit me. A Django minor version update dropped support for a third-party library I was using, and that library hadn’t been updated in two years. Sorting it out meant uninstalling things, reinstalling things, checking which version of what was compatible with what else, and at some point I realised I had accidentally installed half the requirements into the system Python instead of the virtualenv. I still haven’t fully recovered from that.
If you can start fresh: always use a virtual environment. Disturbing system Python packages is its own special kind of DLL hell. It seems fine until it isn’t.
The obvious answer to all of this is Nix. Declare everything, reproduce anything, let the tooling care about compatibility. The problem is that I’m on Windows, and Nix is a Unix tool at its roots. You can get it working through WSL, but that’s a second layer of friction on a machine I use every day for other things, and I kept putting it off.
I’d seen Poetry recommended enough times that I felt vaguely guilty for not trying it. Same with pipx. I just never found the gap in my schedule to sit down and understand either of them properly. Then I saw something about uv and figured I’d spend twenty minutes with it.
I would describe uv as confusingly fast.

§What it actually is
uv is a Python package manager written in Rust, built by Astral, the team behind Ruff. The pitch is that it replaces pip, pip-tools, pipx, poetry, pyenv, and virtualenv with a single tool that is 10 to 100 times faster than pip.That number sounds like marketing copy, but it isn’t. The first install on a fresh machine is already noticeably quicker than pip. The second time, on the same machine or a different project sharing dependencies, is where it gets strange. uv keeps a global cache and deduplicates across projects, so packages you’ve already pulled down don’t get downloaded or even copied again. At some point you stop waiting for the progress bar and start wondering if it ran at all.
Part of this is Rust. Part of it is a more aggressive caching strategy and a global dependency cache that deduplicates across projects. Part of it is just better design: uv doesn’t install a local copy of pip into every virtual environment it creates, which is one of those decisions that seems defensible until you think about it for more than thirty seconds.
The virtual environment story is cleaner too. uv venv creates one, and uv pip install installs into whichever environment is active. The scope is explicit and the commands behave predictably.
§Ruff was like this too
My first real encounter with Ruff had the same quality. I ran it on a project expecting to wait. It came back almost immediately. I actually added a deliberate syntax error to check it was doing anything.
uv gives the same feeling. The first few installs I kept second-guessing the output, looking for signs that something had been skipped. Nothing was skipped. It was just done.
§What it doesn’t fully replace yet
uv doesn’t manage system-level dependencies. If your project needs something that isn’t a Python package, uv won’t install it. That’s not a criticism so much as a scope definition: uv is about Python packages and Python environments, not the whole machine. For the kind of thing Nix handles (or Ansible, for that matter), you still need something else.
The lockfile story is also newer. uv lock generates a uv.lock file that pins the full dependency tree, and uv sync installs from it, which is the reproducibility story. I haven’t used this in production yet, but the design looks right.
I used to love slow pip installs.
They gave me an excuse to take a break. uv took that from me.
