pip, Poetry, and uv: Dependency Resolution and Lockfiles in Python Packaging
Concept
Python's dependency-management story has historically lacked what most other ecosystems take for granted: a fast resolver and a reproducible lockfile, standardized. pip alone (with a plain requirements.txt) does not lock — a fresh pip install -r requirements.txt re-resolves whatever version ranges are pinned against whatever is on PyPI today, which can silently drift between two installs months apart even with identical input files. pip-tools (pip-compile) bolted a lockfile onto pip by resolving once and freezing exact versions plus hashes into a compiled requirements.txt, but its output is platform-specific — a lockfile compiled on Linux doesn't necessarily carry the right wheels for macOS or Windows. Poetry, released as a full project-management tool rather than just a resolver, layers dependency resolution, virtual environment management, build, and PyPI publishing behind one pyproject.toml-driven interface with its own poetry.lock. uv, from Astral (the Ruff team), is the newest entrant: a from-scratch resolver and installer written in Rust, positioned as a drop-in-fast replacement for pip and Poetry's lockfile/env workflow, generating a single cross-platform uv.lock.
The ecosystem is now converging on a real standard for the lockfile format itself: PEP 751, accepted March 2025, defines pylock.toml — a TOML file recording exact versions, file hashes, and installation sources, explicitly designed so an installer (pip, a deploy pipeline) can install from it without doing any resolution work at install time. PEP 751 separates the ecosystem into "lockers" (Poetry, PDM, uv — tools that resolve and produce a lockfile) and "installers" (pip and others that just consume one) — pip itself gained experimental pylock.toml read support in the 26.1 release (April 2026).
Tradeoffs
| Tool | Best for | Cost |
|---|---|---|
pip (+ requirements.txt, no compile step) |
Universal availability — ships with CPython, works everywhere with zero extra install | No real lockfile without pip-tools; version-range installs can drift silently between runs, breaking "works on my machine" reproducibility |
pip-tools (pip-compile) |
Minimal addition on top of plain pip — one extra tool, produces exact-pinned + hashed output | Platform-specific lockfile output (no single cross-platform file); resolution itself is the slowest of the three in head-to-head benchmarks |
| Poetry | Full project lifecycle in one tool — dependency resolution, env management, build, and publish behind one config file; large, mature ecosystem (tens of millions of monthly PyPI downloads) | Slower cold installs and lock generation than uv in current benchmarks; its own resolver/lock format predates and doesn't natively speak PEP 751 pylock.toml |
uv |
Fastest resolution and install by a wide margin (parallel downloads, global content-addressed cache, overlapped metadata fetch/resolve/write); single lockfile resolves for multiple platforms and Python versions from one run | Youngest of the three — less production track record than Poetry; ecosystem/company trajectory is in flux (Astral's 2026 acquisition by OpenAI, not yet closed at time of writing, though uv itself stays open source under MIT/Apache-2.0) |
When to use / when not to
- Reach for
uvon a new project, or when migrating a project whose install/CI time is dominated by dependency resolution — the speed difference (roughly 3s vs. 11s vs. 33s cold-install in comparative benchmarks between uv, Poetry, and pip-tools respectively) compounds meaningfully across CI runs and container rebuilds. - Keep Poetry where it's already entrenched and the team values its unified publish/build workflow over raw resolver speed — a mature Poetry setup with working CI is not worth migrating purely for install-time gains unless that time is a measured bottleneck.
- Use plain
pip(no compile step) only for quick throwaway scripts or environments where reproducibility genuinely doesn't matter — never for anything deployed, since unpinned installs are a real source of "it worked yesterday" production incidents. - Don't adopt a tool-specific lockfile (
poetry.lock,uv.lock) as your interop format for handing dependencies to unrelated tooling — targetpylock.toml(PEP 751) for that, since it's the one format designed to be produced by any locker and consumed by any installer, including pip itself.
Common pitfall
Treating requirements.txt as if it were a lockfile when it only contains loose version specifiers (requests>=2.0). Two installs of the exact same requirements.txt, run weeks apart, can resolve to different transitive dependency versions as new releases land on PyPI — the file looks pinned to a reader but isn't actually reproducible. The fix isn't a discipline problem, it's a tooling gap: compile it (pip-compile) or use a real lockfile-producing tool so exact versions and hashes get frozen, not just top-level version ranges.
Engineering Lens
The practical decision for a team in 2026 is less "which resolver is fastest" and more "which lockfile format will your CI, your container builds, and your teammates' tools all still agree on in two years." PEP 751's pylock.toml matters here less as a competitor to uv/Poetry's own lock formats and more as the interchange layer between them — a team can lock with uv (for speed) while still exporting or targeting a standardized pylock.toml that a plain-pip deploy step, or a different tool entirely, can consume without re-resolving. The migration cost of picking wrong is real but bounded: pyproject.toml itself, the actual source of truth for dependency ranges (as opposed to the resolved lock), is common ground across all three tools, so switching lockers later is a resolver swap, not a project rewrite.
Sources
- PEP 751 – A file format to record Python dependencies for installation reproducibility
- pylock.toml Specification — Python Packaging User Guide
- uv vs Poetry vs pip: Python Package Managers in 2026 — danilchenko.dev
- Python Dependency Management in 2026 — Cuttlesoft