Skip to content

How uv Solves Dependencies So Fast

April 9, 2026·Tim Hopper
uv

Tip

Want more on uv? Browse every uv tutorial, how-to, reference, and explanation on the uv topic page.

Python dependency resolution is NP-hard in the formal sense: it reduces to Boolean satisfiability. In a Jane Street tech talk (YouTube), Charlie Marsh walked through why uv’s resolver has to solve this problem and the architectural decisions that let it do so 10-100x faster than pip.

Python can’t bail out

Rust and Node have an escape hatch: if two packages need different versions of the same dependency, the runtime can load both. Python cannot. Imports use a global cache keyed by module name, so you cannot have Pydantic 1 and Pydantic 2 installed at the same time. If vLLM requires Pydantic 2 and an old version of LangChain requires Pydantic 1, the dependency graph has no solution.

This constraint forces the resolver to search a potentially enormous space of version combinations to find one consistent set. Cargo’s resolver does a graph traversal and uses multi-version as an escape valve when things get hard. uv’s resolver has no such valve. It uses a SAT solver based on CDCL (conflict-driven clause learning), a technique from formal verification that prunes the search space by learning from conflicts. (uv’s implementation builds on the PubGrub algorithm, originally from Dart’s pub.)

Universal lock files require forking the solve

Creating a lock file for a single platform is straightforward: filter out irrelevant markers and solve. But uv aims to produce a universal lock file, one that works on Windows, macOS, Linux, across Python versions, all from a single resolution.

This gets hard fast. A project might legitimately require Pydantic 2 on Windows and Pydantic 1 everywhere else. uv handles this by forking the resolution: it solves separate sub-graphs for each side of a platform marker, then merges the results. Packages that appear in both forks with compatible versions get unified. Packages that differ get annotated with disjoint markers so that at install time, the right version is selected through a simple graph traversal with no second SAT solve.

The markers themselves create a second NP-hard problem. Testing whether two marker expressions are disjoint (can they both be true?) is also Boolean satisfiability. When resolving a project like Hugging Face Transformers with all optional dependencies enabled, individual marker expressions grew to tens of kilobytes before the team built a dedicated marker normalizer based on algebraic decision diagrams.

Charlie described this forking-and-merging system as the hardest part of building uv.

Metadata without downloading the package

Before the resolver can do its work, it needs to know each package’s dependencies. That metadata isn’t always available through the registry API. For packages that only publish source distributions, uv might have to download the source and run setup.py just to discover dependencies.

For wheel archives (which are zip files), uv avoids downloading the full file. It makes a range request for the zip’s central directory (an index at the end of the file), finds the metadata file’s location, then makes a second range request for just that file. For PyTorch wheels that weigh hundreds of megabytes, this means fetching a few kilobytes instead.

Versions as single integers

Python version strings are complex: pre-releases, post-releases, local identifiers, epochs. The full representation involves multiple vectors and heap allocations. But over 90% of real-world versions fit into a single 64-bit integer, packed so that larger versions map to larger integers. Version comparison becomes a single memcmp instead of a multi-field struct comparison. For resolution-heavy workloads with minimal I/O, this optimization alone delivered a 3-4x speedup.

Zero-copy deserialization from cache

uv’s cache stores unpacked wheel contents and links them into virtual environments using hard links or reflinks. Installing a cached package means creating filesystem links, not copying files. This is why recreating a virtual environment with uv feels instant.

For metadata (version lists, dependency graphs), uv uses zero-copy deserialization via the rkyv library. Data is stored on disk in the same binary layout it will have in memory. Reading it back is a pointer cast, not a parse. Unlike JSON deserialization, which scales linearly with data size, this approach has constant deserialization cost regardless of how large the data grows.

As Charlie put it: “The deserialization does not scale with your data.”

Speed changes the relationship

The performance gap changes how developers work with the tool. Virtual environments become ephemeral: destroy and recreate rather than carefully maintain. Tasks that were CI-only, like full dependency resolution or linting the whole codebase, move into local pre-commit hooks. Rebuilding an environment stops being a thing to dread when it finishes faster than a browser tab switch.

At the time of the talk, uv handled over 10% of all PyPI requests and had reached 16 million downloads per month.

The full talk covers additional topics including uv’s pip-compatible interface, the install plan system, and how uv discovers Python interpreters. Watch it at Jane Street’s site or on YouTube.

Last updated on

Please submit corrections and feedback...