Why uv is so fast, and what a dependency resolver actually does
One of the fastest-growing tools in the Python world right now is uv, a package manager from Astral (the people behind Ruff). The headline number is the speed: 8 to 10x faster than pip cold, and 80 to 115x faster with a warm cache. Recreating a virtualenv that used to take a minute now takes under a second.
The speed is the reason people switch. But it is the boring half of the story. The interesting half is what your package installer is actually doing when it "resolves dependencies," because it is quietly solving a genuinely hard problem, and understanding that problem explains both why pip was slow and why it sometimes gave up.
The one idea: installing is a search problem
When you run pip install pandas, you are not just downloading pandas. Pandas needs numpy. Maybe your project also needs scipy, which also needs numpy, but a different range of numpy versions. The installer has to find one version of numpy that satisfies everyone at once, for every package in the whole transitive tree.
That is not a download. That is a constraint-satisfaction search:
Find an assignment of one version to each package such that every package's version requirements are satisfied simultaneously.
If that sounds like a logic puzzle, it is. It is in the same family as SAT (boolean satisfiability), the canonical hard search problem. Most of the time there is an easy answer. Sometimes there is none, and the resolver has to prove that and tell you, which is the dreaded "cannot find a compatible set of versions."
Why it can be slow: backtracking
A naive resolver picks the newest version of each package and hopes. When two requirements conflict, it has to backtrack: undo a choice, try an older version, and re-check everyone. Picture the conflict:
your-app needs numpy >= 2.0
old-plugin needs numpy < 2.0
There is no single numpy that satisfies both. A resolver may try numpy 2.1, discover old-plugin rejects it, back up, try 1.26, discover your-app rejects it, and so on. With deep trees and many packages, the number of combinations explodes. Old pip versions could spend minutes flailing here, or pick something that "worked" but quietly violated a constraint.
The fix is not to search faster, it is to search smarter: when a conflict happens, learn why and avoid every future combination that would repeat it. That is exactly what modern resolvers do.
How uv makes it fast
uv's speed comes from attacking the problem on several fronts at once:
- A real resolution algorithm (PubGrub). Instead of blind backtracking, PubGrub does conflict-driven resolution: when it hits an incompatibility, it derives a general rule ("any version of numpy ≥ 2.0 conflicts with old-plugin") and uses it to prune huge swaths of the search space at once. It also produces human-readable explanations when no solution exists, instead of a wall of attempts.
- Written in Rust. The resolver, the downloader, and the installer are native code, not interpreted Python doing the heavy lifting. No per-operation interpreter overhead.
- Parallel everything. Metadata fetches and downloads happen concurrently, not one package at a time.
- A global cache with hardlinks. uv keeps one global module cache and, on supported filesystems, hardlinks files into each virtualenv instead of copying bytes. That is why a warm cache is ~80x faster: there is almost nothing to download or copy, it is wiring up references to files already on disk.
- A forking resolver. When requirements differ by platform or Python version (a package needed only on Windows, say), uv splits resolution along those markers instead of trying to force one universal answer. This handles the messy real-world cases that make naive resolvers choke.
Notice the pattern: one part is "use a better algorithm," and the rest is "stop doing redundant work" (cache, hardlink, parallelize, don't re-resolve what hasn't changed). That combination, not any single trick, is the 80x.
Why this is worth understanding
It is tempting to file uv under "fast pip" and move on. But the lesson generalizes: any time a tool resolves versions, it is solving a search problem, and its quality is judged on three things — does it find a valid solution when one exists, does it prove none exists when one doesn't (with a readable reason), and does it avoid redundant work. npm, Cargo, apt, and Poetry are all wrestling with the same puzzle. Once you see the puzzle, "dependency hell" stops being mysterious; it is just an over-constrained search.
And the speed matters more than convenience. A resolver that is 80x faster changes behavior: you stop avoiding clean rebuilds, you pin and lock aggressively because it is cheap, and your CI gets faster on every run. Tooling speed compounds.
If you want to build the intuition for problems like this, search with constraints, backtracking, and the difference between "no answer" and "gave up", that is the kind of thing the general coding track is built around: not memorizing the tool, but understanding the problem the tool is solving.