Python Deployment 2026-08-25

uv vs pip: what's the difference and why it matters

For years, pip has been the default way to install Python packages. Recently, uv - a package and project manager written in Rust by Astral (the makers of Ruff) - has been gaining popularity as a drastically faster, all-in-one replacement. In this post, we'll look at the theory behind both tools, compare their architectures, and go through practical examples.

The Theory: How pip Works

pip is the standard package installer bundled with Python. When you run pip install, it roughly does the following:

  1. Resolve dependencies one at a time, using a backtracking resolver written in pure Python.
  2. Download package metadata and wheels/sdists from PyPI (or another index) over HTTP.
  3. Build sdists if no wheel is available (which can require a C compiler and system headers).
  4. Install the resulting files into the active virtual environment, writing to disk sequentially.

This design is simple and reliable, but it's also single-threaded for network I/O and dependency resolution, which becomes slow on projects with many dependencies.

The Theory: How uv Works

uv reimplements the same job in Rust, with a few key architectural differences:

  • Parallel, async I/O - metadata and packages are downloaded concurrently instead of one by one.
  • Global cache with hard links - once a wheel is downloaded, it's cached and hard-linked into every virtual environment that needs it, so re-installing the same version is nearly instant and uses no extra disk space.
  • A faster resolver - written in Rust, using the same PubGrub-style algorithm as Cargo, which resolves complex dependency graphs much faster than pip's resolver.
  • Built-in project & Python management - unlike pip, uv can also create virtual environments, manage pyproject.toml, lock dependencies (uv.lock), and even install Python interpreters themselves.
Architecture at a Glance
Your command
uv add django
Rust resolver
(PubGrub-style)
Global cache
(hard-linked wheels)
Virtual env
+ uv.lock

Compare this to pip, which resolves and downloads sequentially and installs straight into the active virtual environment, with no shared cache between projects by default:

Your command
pip install django
Sequential resolver
(pure Python)
Virtual env
(no lockfile by default)
Examples: Installing a Package

With pip, you typically create a virtual environment first, then install packages into it:

python -m venv .venv && source .venv/bin/activate pip install django requests Successfully installed django-6.1 requests-2.32.0

With uv, the virtual environment is created and managed for you automatically:

uv add django requests Resolved 4 packages in 12ms Prepared 2 packages in 45ms Installed 2 packages in 3ms

Locking and Reproducibility

Pip's answer to reproducible installs is pip freeze > requirements.txt, which only captures whatever happens to be installed at that moment:


pip freeze > requirements.txt
pip install -r requirements.txt
              

uv generates and maintains a proper cross-platform lockfile automatically as part of pyproject.toml-based projects:


uv lock          # creates/updates uv.lock
uv sync          # installs exactly what's in uv.lock
              

Installing from an Existing requirements.txt

uv is also a drop-in replacement for pip's command-line interface, so migrating existing projects is straightforward:


# Old way
pip install -r requirements.txt

# New way (much faster, same result)
uv pip install -r requirements.txt
              
Speed: Why It Matters

The main reason uv has become popular so quickly is raw speed. Because it resolves dependencies and downloads/installs packages in parallel using Rust, and reuses a global cache via hard links, cold installs are commonly 10-100x faster than pip, and warm (cached) installs can be close to instantaneous. In CI pipelines, Docker builds, and local development loops, that difference adds up quickly - fewer minutes waiting for pip install to finish means faster feedback and lower CI costs.

Aspect pip uv
Language Python Rust
Dependency resolution Sequential, pure-Python backtracking Parallel, PubGrub-style (like Cargo)
Caching Per-environment, limited Global cache with hard links
Lockfile Not built-in (requirements.txt workaround) Built-in (uv.lock)
Virtualenv & Python management Separate tools needed (venv, pyenv) Built-in
Typical install speed Baseline 10-100x faster

When Should You Still Use pip?

pip remains the safest default in environments where you can't install extra tooling, in minimal Docker base images where every extra binary matters, or when following documentation that assumes a plain pip workflow. It's also still the universal fallback: uv itself uses pip's dependency metadata format and can act as a pip-compatible interface (uv pip ...), so you're never locked out of the ecosystem.

Conclusion

uv and pip solve the same problem - installing Python packages - but with very different levels of ambition. pip is the stable, universally available baseline that ships with Python itself. uv is a much faster, more complete reimplementation that also manages virtual environments, lockfiles, and Python versions. For new projects, especially in this Django project which already uses uv.lock, uv is the clear choice; for quick one-off scripts or constrained environments, plain pip still gets the job done.


Scientific Dev
Scientific Dev
Educator & Developer