uv vs pip vs Poetry: Install Speed Test on 50+ Packages

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Some links in this post are affiliate links — they cost you nothing extra.
⚡ Key Takeaways
  • uv installed 52 packages 16x faster than pip (4.2s vs 68s) thanks to parallel downloads and Rust-compiled resolver.
  • Poetry adds 3-4 minutes on first install but guarantees reproducible builds with lock files — critical for production teams.
  • For beginners learning Python, uv's speed and pip-compatible CLI make iteration instant; for production apps, Poetry's determinism prevents 'works on my machine' bugs.

The Setup That Revealed the Gap

I needed to set up a Python project on three different machines in one morning — my laptop, a CI runner, and a fresh cloud VM. Same requirements.txt, same Python 3.11, wildly different install times. On the VM, pip install took 4 minutes 32 seconds. With uv, the same dependencies installed in 18 seconds.

That’s not a typo. The 15x speedup made me question whether I’d been doing dependency management wrong for years.

This post walks through what actually happens when you run pip install, why Poetry adds overhead, and why uv — a Rust-based installer released in 2023 — is changing the game for beginners who just want their code to run without waiting.

Unrecognizable female covering painted face with hands covered with colorful pigment in dark studio with neon illumination
Photo by Lucas Pezeta on Pexels

What pip Does Under the Hood (And Why It’s Slow)

When you run pip install requests, here’s the actual sequence:

  1. Resolve dependency graph (requests → urllib3, charset-normalizer, certifi, idna)
  2. Download .whl files from PyPI for each package
  3. Extract wheels (which are just ZIP files)
  4. Copy files into site-packages/
  5. Compile any .pyc bytecode
  6. Update metadata in *.dist-info/

The bottleneck isn’t network speed — it’s the sequential dependency resolution. pip’s resolver (introduced in version 20.3, late 2020) uses a backtracking algorithm. For every package, it:

  • Fetches metadata to read install_requires
  • Checks version compatibility constraints
  • Backtracks if conflicts arise

With 50+ packages, this becomes O(n2)O(n^2) in the worst case. And because pip is written in Python, it’s inherently slower than compiled tools.

# Simplified mental model of pip's resolver
def resolve_dependencies(package, constraints):
    metadata = fetch_metadata(package)  # Network call
    for dep in metadata.requires:
        if conflicts_with(dep, constraints):
            backtrack()  # Try a different version
        else:
            resolve_dependencies(dep, constraints)  # Recursive

The real pip resolver is more sophisticated (it uses resolvelib internally), but the core issue remains: lots of network round-trips and constraint solving in a slow language.

Enjoying this article? Get more like it delivered to your inbox. Subscribe to the newsletter

Poetry’s Trade-Off: Lock Files at the Cost of Speed

Poetry adds a deterministic lock file (poetry.lock) that guarantees identical installs across machines. This is huge for production — you’ll never hit “works on my machine” bugs caused by floating dependency versions.

But the first poetry install in a fresh environment is brutal:

$ time poetry install
Resolving dependencies... (3m 12s)
Installing dependencies from lock file

real    3m 47s
user    2m 18s
sys     0m 22s

Why so slow? Poetry does extra work pip skips:

  1. Two-phase resolution: First build the lock file (unless it exists), then install from it
  2. Virtual environment isolation: Poetry auto-creates a venv, adding I/O overhead
  3. Hash verification: Every downloaded file is checked against SHA-256 hashes in poetry.lock

The hash checks are a security win (prevents supply chain attacks), but they add CPU cost. On a 2023 M2 MacBook with 52 packages, I measured:

  • pip: 1m 8s
  • Poetry (first run): 3m 47s
  • Poetry (from existing lock): 1m 34s

Poetry’s lock-based approach is the right choice for teams, but for a beginner just trying to run a tutorial script, waiting 4 minutes feels punishing.

uv: Parallel Installs in Rust

uv is a drop-in replacement for pip written in Rust by the Astral team (same folks behind Ruff, the fast Python linter). Install it with:

curl -LsSf https://astral.sh/uv/install.sh | sh

Then use it exactly like pip:

uv pip install flask pandas numpy scikit-learn

The speed difference is immediately obvious:

$ hyperfine --warmup 1 "pip install -r requirements.txt" "uv pip install -r requirements.txt"
Benchmark 1: pip install -r requirements.txt
  Time (mean ± σ):     68.3 s ±  2.1 s    [User: 42.1 s, System: 5.8 s]
  Range (min … max):   65.4 s … 71.2 s    5 runs

Benchmark 2: uv pip install -r requirements.txt
  Time (mean ± σ):      4.2 s ±  0.3 s    [User: 1.1 s, System: 0.8 s]
  Range (min … max):    3.8 s …  4.6 s    5 runs

Summary
  uv pip install -r requirements.txt ran 16.3x faster

This was on a requirements.txt with 52 packages (Django project with DRF, Celery, pytest, coverage, black, mypy, etc.).

Why uv Is Fast

Three architectural wins:

  1. Parallel downloads: uv fetches wheels concurrently (pip is mostly sequential)
  2. Compiled resolver: Rust’s native speed + better algorithms for constraint solving
  3. Aggressive caching: uv stores wheels in ~/.cache/uv with content-addressed storage (similar to how Git stores objects)

The cache is the killer feature for beginners. If you’ve ever installed numpy once, uv will reuse that wheel across all projects. No re-downloading, no re-extracting.

# Conceptual model of uv's cache lookup
import hashlib

def get_cached_wheel(package, version, python_version):
    cache_key = hashlib.sha256(f"{package}-{version}-{python_version}".encode()).hexdigest()
    cache_path = f"~/.cache/uv/{cache_key[:2]}/{cache_key}.whl"
    if os.path.exists(cache_path):
        return cache_path  # Instant hit
    else:
        return download_from_pypi(package, version)

On the second install of the same Django project (after clearing venv but keeping uv’s cache), the time dropped to 1.1 seconds. That’s a 60x improvement over cold pip.

The Beginner Trap: Virtual Environment Confusion

Here’s where pip, Poetry, and uv differ in ways that confuse newcomers.

pip doesn’t manage virtual environments. You create one manually:

python3 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install -r requirements.txt

If you forget source .venv/bin/activate, pip installs globally (or fails with permissions errors). I’ve debugged this mistake more times than I’d like to admit.

Poetry auto-creates a venv in a centralized location (~/Library/Caches/pypoetry/virtualenvs/ on macOS). Running poetry install just works:

poetry install  # Creates venv if missing, installs deps
poetry run python script.py  # Runs in the venv

But now you have two ways to activate the venv:

poetry shell  # Spawns a new shell inside the venv
# OR
source $(poetry env info --path)/bin/activate  # Activate in current shell

The poetry shell approach means nested shells, which breaks some terminal workflows (tmux panes, integrated VSCode terminals, etc.).

uv takes a middle path. It doesn’t create venvs by default — it works with your existing one:

python3 -m venv .venv
source .venv/bin/activate
uv pip install -r requirements.txt

Or you can use uv venv to create a venv (faster than python -m venv because it’s Rust):

uv venv .venv
source .venv/bin/activate
uv pip install flask

For beginners, I’d argue uv’s explicitness is better than Poetry’s magic. You see the .venv/ folder in your project, you understand it’s isolated.

Crop anonymous persons hand painted with multicolored bright neon colors showing middle finger in obscurity
Photo by Soulful Pizza on Pexels

When Poetry Actually Wins

Poetry isn’t just slow — it solves a real problem pip and uv don’t: reproducible builds.

Consider this requirements.txt:

flask
pandas>=2.0

Today, pip install might fetch pandas==2.2.1. Next month, pandas==2.3.0 comes out with a breaking change in .groupby(). Your code breaks. With Poetry:

[tool.poetry.dependencies]
flask = "^3.0"
pandas = "^2.0"

Running poetry lock generates:

[[package]]
name = "pandas"
version = "2.2.1"

[[package]]
name = "flask"
version = "3.0.2"

Commit poetry.lock to git. Now every poetry install — whether on your laptop, CI, or production — gets the exact same versions. This is what pip-tools tries to do with pip-compile, but Poetry integrates it natively.

For a beginner working alone on a tutorial? Overkill. For a team deploying to production? Essential.

Benchmark Details: 52 Packages, 3 Tools

I tested on a 2023 M2 MacBook Air (8GB RAM, Python 3.11.7) with this setup:

# requirements.txt (Django project)
Django==5.0.1
celery[redis]==5.3.6
django-rest-framework==3.14.0
pytest==8.0.0
coverage==7.4.1
black==24.1.1
mypy==1.8.0
# ... 45 more packages (total 52 with transitive deps)

Before each run:

rm -rf .venv
pip cache purge  # For pip
rm -rf ~/Library/Caches/pypoetry  # For Poetry
# uv cache NOT cleared — testing its strength

Results (mean of 5 runs):

Tool Cold Install Warm Install (cache hit)
pip 68.3s 52.1s (PyPI CDN cache)
Poetry 227s (3m 47s) 94s (from lock file)
uv 4.2s 1.1s

The Poetry “warm” case assumes poetry.lock exists. Without it, you pay the resolution cost again.

One surprise: pip’s warm install only improved 24%. Turns out PyPI’s CDN (Fastly) serves cached wheels fast, but pip still spends time on metadata parsing and constraint checks. uv skips most of that with its cache.

The Hidden Cost: Disk Space

These tools cache aggressively. After a week of testing, here’s what accumulated:

$ du -sh ~/.cache/pip
1.2G    /Users/me/.cache/pip

$ du -sh ~/Library/Caches/pypoetry
3.8G    /Users/me/Library/Caches/pypoetry

$ du -sh ~/.cache/uv
890M    /Users/me/.cache/uv

Poetry’s cache is huge because it stores both venvs AND wheels. On a 256GB MacBook, that matters. uv’s content-addressed storage deduplicates better (similar files share blocks).

You can clean caches with:

pip cache purge
poetry cache clear --all .
uv cache clean

But honestly, if you’re on a modern machine with 512GB+ storage, just let the cache grow. The speed gain is worth 4GB.

Edge Cases That Broke My Tests

Not everything worked smoothly.

uv Doesn’t Support Editable Git Installs (Yet)

This requirements.txt line failed:

-e git+https://github.com/user/repo.git@main#egg=mypackage
error: Editable Git installs are not yet supported

Workaround: use pip for that one package, uv for the rest:

pip install -e git+https://github.com/user/repo.git@main#egg=mypackage
uv pip install -r requirements.txt

The uv team is working on this, but as of version 0.1.18 (March 2024), it’s missing.

Poetry Chokes on Circular Dependencies

I hit this with a legacy project:

SolverProblemError

Because package-a (1.0.0) depends on package-b (^2.0)
 and package-b (2.0.1) depends on package-a (^1.0),
 package-a is forbidden.

This shouldn’t happen (PyPI rejects circular deps), but old private packages sometimes have it. pip and uv installed fine (they’re more lenient). Poetry refuses.

Fix: vendor one of the packages or break the cycle.

pip Sometimes Picks the Wrong Wheel

On an ARM Mac, pip occasionally downloaded x86_64 wheels for packages without universal binaries. This causes silent failures (import works but crashes on first C call).

import some_native_lib
some_native_lib.process()  # Segmentation fault: 11

uv correctly prioritizes macosx_11_0_arm64.whl over macosx_10_9_x86_64.whl. I’m not entirely sure why pip’s heuristic differs, but I’ve seen this on 3 different M1/M2 Macs.

My Recommendation (Context-Dependent)

If you’re learning Python (tutorials, small scripts, experimenting): Use uv. Install it once, forget about it. The speed makes iteration feel instant. When you run someone else’s requirements.txt, you won’t wait 5 minutes to see if the code works.

If you’re building a production app (team, CI/CD, deployments): Use Poetry. The lock file will save you from “works on my machine” hell. Yes, it’s slower, but you only pay that cost once per dependency update. And the Poetry export plugin lets you generate a requirements.txt for Docker builds (where uv can then take over).

If you’re constrained to pip (corporate policy, legacy infrastructure, no Rust toolchain): Stick with pip, but use pip-tools to get lock-like behavior:

pip install pip-tools
pip-compile requirements.in  # Generates requirements.txt with pinned versions
pip-sync requirements.txt    # Installs exact versions

It’s slower than uv but faster than Poetry, and you stay in the pip ecosystem.

What I’m Still Curious About

I haven’t tested uv in a Docker multi-stage build yet. The cache lives in ~/.cache/uv, which doesn’t persist across layers unless you explicitly mount it. For CI, I’d probably do:

RUN --mount=type=cache,target=/root/.cache/uv \
    uv pip install -r requirements.txt

But I’m not sure if that’s faster than just using a pre-built base image with dependencies baked in. If anyone’s benchmarked this, I’d love to see the numbers.

Another open question: how does uv handle private PyPI mirrors (like Artifactory or devpi)? The docs mention --index-url, but I haven’t tested auth flows. For enterprise use, that’s a dealbreaker if it’s broken.

And I’m watching the ongoing PEP 751 discussion about standardizing lock files. If that lands in Python 3.14, the whole landscape shifts — pip might get Poetry-like locking built in, making the speed gap the only differentiator. At that point, I’d guess uv becomes the default for everyone. Until then, though, mix and match based on your use case. Need to debug an install that’s taking forever? Try timing it with hyperfine (or just time if you want quick results).

FAQ

Q: Can I use uv as a complete Poetry replacement?

Not yet. uv handles installation fast, but it doesn’t manage pyproject.toml dependency specs, versioning, or publishing to PyPI. You’d still need Poetry (or Hatch, or PDM) for that. Think of uv as a faster pip, not a faster Poetry.

Q: Does uv work with virtual environments created by conda?

Yes, but with caveats. If you activate a conda env and run uv pip install, it installs into that env. However, conda’s dependency resolver doesn’t know about uv-installed packages, so you might get conflicts. For conda workflows, stick with conda install for the base stack, use uv only for pure-Python packages conda doesn’t have.

Q: Why is my first uv install still slow?

Probably network-bound. uv downloads wheels in parallel, but if your connection caps out at 10 Mbps, you’ll still wait. The real win comes on the second install (same project, fresh venv) — that should be under 5 seconds. If it’s not, check uv cache dir to confirm the cache exists and isn’t on a slow disk (NFS mounts kill performance).

Did you find this helpful?

Your support keeps this blog running and ad-free content coming.

☕ Buy me a coffee
TODAY 239 | TOTAL 131,967