Contents
  1. pip and the Global Environment Problem
  2. pipx and Isolated Tool Environments
  3. Poetry and Reproducible Project Environments
  4. uv and the Global Cache Model
  5. Why I Personally Moved to Poetry
  6. A Note on VSCode Integration
  7. What Changed and What Did Not
  8. What You Can Do Now
← All posts

From pip to uv: How Python Package Management Evolved

pip made Python packages installable. pipx isolated CLI tools. Poetry brought lockfiles and reproducibility. uv rethought the entire model with a global cache and hard links. Each step was a response to a real structural problem.

pip made it possible to install Python packages with a single command. For a long time, that was enough. But as projects became more complex and teams larger, the model pip relied on started creating real problems: conflicting global dependencies, non-reproducible installs, and no clear boundary between tools you run and libraries your project depends on. pipx, Poetry, and uv each represent a distinct architectural response to those problems.

pip and the Global Environment Problem

pip installs packages into whichever Python environment is currently active. If no virtual environment is active, that defaults to the system Python, which means packages accumulate in a single shared location across every project on the machine.

The consequence is dependency conflict. If project A requires version 1 of a library and project B requires version 2, only one can exist in the global environment at a time. The standard workaround is to create a virtual environment per project using venv or virtualenv, which creates an isolated Python installation that pip installs into. But pip itself has no concept of a project, no lockfile, and no mechanism to ensure reproducible installs. A requirements.txt file lists package names and optional version constraints, but it does not record the full resolved dependency tree. Two developers running pip install -r requirements.txt at different times can receive different transitive dependency versions.

pip also has no concept of separating CLI tools from project libraries. Installing a CLI tool like black or pytest globally means it shares the environment with every other package, which is a source of conflicts and makes clean uninstalls difficult.

pipx and Isolated Tool Environments

pipx addresses the CLI tool problem specifically. When you install a tool with pipx install, it creates a dedicated virtual environment for that tool alone and exposes its binaries on your system PATH. The tool is usable everywhere but lives in complete isolation from other tools and from any project environment.

This is architecturally distinct from pip in that pipx never installs into a shared environment. Each tool owns its environment entirely. Uninstalling a tool removes that environment and leaves nothing behind. No other tool is affected. pipx does not replace pip for project dependency management. It solves the narrower problem of managing executables safely.

Poetry and Reproducible Project Environments

Poetry introduced a project-centric model to Python dependency management. Instead of a requirements.txt, Poetry uses a pyproject.toml to define dependencies and a poetry.lock file to record the exact resolved version of every package and its transitive dependencies. When another developer runs poetry install, they receive the exact same packages recorded in the lockfile, not whatever the latest compatible versions happen to be at that moment.

Poetry also manages virtual environments automatically. By default it creates them in a central cache directory, though it can be configured to place them inside the project directory. It detects and respects existing activated environments. This is more than pip provides by default, where the developer is responsible for creating, activating, and managing the virtual environment manually.

The poetry.lock file is what makes Poetry’s model reliable in teams. It is the authoritative record of a working dependency tree, committed to version control so that every environment, local, CI, production, resolves identically.

uv and the Global Cache Model

uv takes the architectural step that pip and Poetry do not: it introduces a shared global cache backed by hard links, similar to what pnpm did for JavaScript.

When uv installs a package, it stores the package files in a central content-aware cache directory on the machine. When a project needs that package, uv hard-links the files from the cache into the project’s virtual environment instead of copying them. A hard link is a second directory entry pointing to the same data on disk, not a duplicate. If ten projects depend on the same version of a package, the files exist once on disk and are referenced ten times. Disk usage is paid once regardless of how many projects share the dependency.

uv also produces a uv.lock file that serves the same purpose as poetry.lock: a full record of the resolved dependency tree for reproducible installs. The virtual environment itself is placed in a .venv directory inside the project by default.

The resolution engine in uv is written in Rust, which makes dependency resolution and installation significantly faster than pip or Poetry’s Python-based implementations. The speed difference is measurable on any project with a non-trivial dependency tree.

Why I Personally Moved to Poetry

The reason I started using Poetry was not speed or lockfiles. It was uninstallation. When you run pip uninstall pandas, pip removes pandas and nothing else. All the packages that pandas pulled in as dependencies, numpy, python-dateutil, pytz, and others, remain in your environment untouched. Over time, environments accumulate orphaned packages from libraries you no longer use, with no clean way to identify or remove them. Poetry tracks the full dependency graph and knows which packages exist solely because another package required them. When you remove a dependency with poetry remove, it removes the package and every transitive dependency that is no longer needed by anything else in the project.

That single behaviour changed how I managed Python environments.

A Note on VSCode Integration

I used uv during a debugging session in VSCode and ran into an issue that is worth being direct about. The VSCode Python extension did not automatically detect the .venv that uv created, which meant the interpreter was not selected and imports were flagged as unresolved. With Poetry, the extension’s integration has been more reliable in my experience, likely because Poetry’s environment management model has been stable for longer and the extension has had time to build reliable support for it.

This is a documented issue. The uv project marked the VSCode detection problem as external, meaning the responsibility for fixing it lies with the VSCode Python extension rather than uv itself. The situation may have improved in more recent versions of both tools. If you are using uv in VSCode, you may need to manually select the interpreter from the .venv folder in your project root until automatic detection is consistently supported.

What Changed and What Did Not

pip established the model: a registry, a command to install from it, and a local environment to install into. Every tool after it kept the registry. What changed was the isolation model, the reproducibility model, and the storage model.

pipx solved tool isolation. Poetry solved reproducibility with a lockfile and automated environment management. uv solved disk efficiency and speed through a global cache with hard links, while also providing a lockfile. These are distinct architectural positions, not incremental improvements to the same design.

What You Can Do Now

The architectural differences between these tools become concrete the moment you run them. Work through each of the following.

Compare install speed. Set up the same project with a non-trivial dependency list and time each tool:

# pip
time pip install -r requirements.txt

# poetry
time poetry install

# uv
time uv sync

Compare disk usage. After each install, measure the environment size:

du -sh .venv

For uv specifically, also check what the global cache holds across all your projects:

uv cache dir
du -sh $(uv cache dir)

See the orphan problem yourself. Create a fresh virtual environment, install pandas with pip, then uninstall it:

pip install pandas
pip uninstall pandas
pip list

Observe how many packages remain. Then do the same with Poetry:

poetry add pandas
poetry remove pandas
poetry show

The difference in what is left behind is the argument for Poetry in a single command.

Run a security audit. Point each tool at the same dependency set and compare what they surface:

pip-audit
poetry check
uv audit

Check the lockfile. Open poetry.lock or uv.lock and compare it to a requirements.txt. The lockfile records every transitive dependency with its exact resolved version and hash. The requirements file records only what you wrote manually. That difference is what reproducibility means in practice.

← All posts