backend / python core / packaging / 01_pip_poetry_uv.md

Python Packaging — pip, Poetry, uv

5 interview angles 6 min read source

Python Packaging — pip, Poetry, uv

How Python projects manage dependencies. The landscape moved fast — pip + requirements.txt, then Poetry/PDM, now uv. Knowing the trade-offs and why the field evolved is the senior signal.

The tools

Tool What it is
pip the built-in installer. Installs packages; does not, by itself, manage a project, lock dependencies, or resolve cleanly.
pip + pip-tools pip plus pip-compile to generate a locked requirements.txt from a loose input. The “minimal” modern setup.
Poetry full project manager: dependency declaration, resolution, locking, virtualenv management, building, publishing. pyproject.toml-native.
PDM similar scope to Poetry, standards-forward (PEP 582/621).
uv Astral’s Rust-based tool: a pip/pip-tools/virtualenv/Poetry replacement that is dramatically faster. The current momentum is here.
conda separate ecosystem; manages non-Python deps too (C libs, CUDA). Common in data science, rare in web backends.

What “pip alone” doesn’t do — and why the others exist

pip install requests works. The problems at a project/team scale:

  • No lockfile. pip freeze > requirements.txt captures what’s installed, but it’s flat — it doesn’t distinguish your direct deps from their transitive deps, and it’s not generated from a clean resolution.
  • No separation of “what I want” vs “what got resolved.” You want “Django >= 5.2”; you need to record “Django 5.2.6 + its exact 30 transitive deps.” Plain pip conflates these.
  • No dev/prod groups. pytest and mypy shouldn’t ship to production; plain requirements.txt has no notion of groups.
  • Resolution was historically weak — older pip would happily install an inconsistent set; modern pip has a real resolver but still no lockfile.

So the ecosystem built tools that add: a declared dependency spec (loose constraints), a locked resolution (exact pinned graph), dependency groups, and integrated virtualenv handling.

pip + pip-tools — the minimal modern setup

requirements.in        # what you declare: "django>=5.2", "celery"
   │  pip-compile

requirements.txt       # the lock: every package + transitive, pinned, hashed
   │  pip-sync

the virtualenv         # exactly the locked set, nothing more

pip-compile resolves and pins; pip-sync makes the environment match the lock exactly (removes things not in it). Lightweight, uses only pip under the hood, no new project model. A reasonable choice for teams that want minimal tooling.

Poetry — the integrated project manager

# pyproject.toml
[tool.poetry.dependencies]
python = "^3.12"
django = "^4.2"
celery = "^5.3"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
mypy = "^1.11"
  • poetry add django — declares it, resolves, updates poetry.lock.
  • poetry install — creates/uses a virtualenv, installs the locked set.
  • poetry.lock — the full pinned, hashed dependency graph; commit it.
  • Handles build + publish too (poetry build, poetry publish).

One tool for declare/resolve/lock/venv/build. Poetry’s own resolver was historically slow on large graphs (a real complaint) — which is part of why uv gained ground.

uv — the current momentum

uv (Astral, the ruff people) is a Rust-based tool that replaces pip, pip-tools, virtualenv, and much of Poetry — and is 10-100× faster at install and resolution.

uv venv                    # create a virtualenv (instant)
uv pip install django      # pip-compatible install, much faster
# or the project workflow:
uv add django              # declare + resolve + lock (uv.lock)
uv sync                    # make the venv match the lock
uv run pytest              # run a command in the project env
  • Drop-in uv pip commands for the pip-compatible path.
  • A full project workflow (uv add/uv sync/uv.lock) for the Poetry-style path.
  • Also manages Python versions themselves (uv python install 3.12) — subsumes pyenv.
  • The speed isn’t a gimmick: it changes CI times and local iteration meaningfully.

For new projects in 2025+, uv is the increasingly common default. The interview-relevant point is why: the field consolidated toward one fast tool that does everything (versions, venvs, resolution, locking, running), after a decade of stitching pip + virtualenv + pyenv + pip-tools/Poetry together.

What they all share — the underlying model

Regardless of tool, the model is the same and worth stating clearly:

  1. Declare loose constraints (django>=5.2) in pyproject.toml (or requirements.in).
  2. Resolve the full graph to a consistent set of exact versions.
  3. Lock that resolution to a file (poetry.lock, uv.lock, pinned requirements.txt) — commit it.
  4. Sync an environment to match the lock exactly.

The tool is an implementation detail; the model — declared spec → resolved lock → reproducible environment — is the thing. See 02_virtualenvs_and_lockfiles.md and 03_pyproject_and_building.md.

Choosing in practice

Situation Reasonable choice
New project, 2025+ uv — fast, does everything, growing standard
Existing Poetry project stay on Poetry; migrate to uv when convenient, not urgently
Minimal tooling, pip-only shop pip + pip-tools
Data science with non-Python deps conda / mamba (different ecosystem)
Library you publish to PyPI uv or Poetry (both build + publish); see the building file

Don’t churn a working setup for fashion. But for greenfield, uv is the easy recommendation.

Common gotchas

  • pip freeze as a lockfile — it’s a flat snapshot of what’s installed, not a resolution; doesn’t separate direct from transitive, isn’t reproducible. Use a real lock.
  • Not committing the lockfile — then “it works on my machine” is back; the lock is the reproducibility.
  • pip install straight into a global/system Python — pollutes it, version conflicts across projects. Always a per-project virtualenv.
  • Mixing toolspip install into a Poetry-managed venv silently desyncs it from poetry.lock. Pick one tool per project and use it consistently.
  • conda + pip mixed carelessly — each can clobber the other’s view of the environment. If you use conda, install via conda where possible.
  • Slow Poetry resolution blamed on “Python packaging” — often just the resolver; uv is the answer if speed is the pain.

Interview angle

  • “Why isn’t pip + requirements.txt enough?” — plain pip has no lockfile distinct from a flat install snapshot, no separation of declared vs resolved deps, no dev/prod groups. pip freeze captures what’s installed, not a clean reproducible resolution. The ecosystem built pip-tools / Poetry / uv to add declared specs, real locks, and groups.
  • “pip vs Poetry vs uv?” — pip: the installer, no project management. Poetry: integrated declare/resolve/lock/venv/build, but historically slow resolution. uv: Rust-based, 10-100× faster, replaces pip + virtualenv + pyenv + much of Poetry, and is the current default for new projects. The model underneath is identical — declare, resolve, lock, sync.
  • “What does a packaging tool actually do for you?” — turns loose declared constraints (django>=5.2) into a resolved, exact, locked dependency graph, manages the per-project virtualenv, and syncs the environment to match the lock — giving reproducible installs across machines and CI.
  • “What would you pick for a new service?” — uv: it’s fast (real CI/iteration impact), does versions + venvs + resolution + locking + running in one tool, and the ecosystem is consolidating on it. For an existing Poetry project I wouldn’t churn it just for fashion.
  • “Why did Python packaging have so many tools?” — historically you stitched together pip (install) + virtualenv (isolation) + pyenv (versions) + pip-tools/Poetry (locking) because no single tool did it all well. uv consolidating those is the field finally converging.