What is a package manager?

A package manager is software that installs, updates, removes, and keeps track of other software packages, together with the constraints between them. Operating systems have long had their own (apt on Debian/Ubuntu, brew on macOS, winget on Windows); Python has its own ecosystem, built around the Python Package Index (PyPI) as the central public repository.

In Python, a package is reusable code published so that other people do not need to write it themselves. For example:

Dependencies, and why resolution is hard

A package can itself depend on other packages, which may depend on others in turn. These indirect requirements are called transitive dependencies. Installing pandas, for example, pulls in numpy as a dependency without you asking for it by name.

This quickly produces a dependency graph rather than a simple list. Two direct dependencies might place incompatible constraints on a shared transitive dependency — package A might require numpy>=1.24, while package B only supports numpy<1.24.

Finding a single set of versions that satisfies every constraint is known as dependency resolution. In general, this is a computationally hard problem, closely related to the Boolean satisfiability problem (SAT). This is exactly the kind of tedious and error-prone work that a package manager automates.

Historically, Python’s own installer, pip, used comparatively simple dependency handling and, for a long time, did not fully resolve dependency conflicts. This is one reason the community has built more sophisticated tools such as pip-tools, Poetry, PDM, conda, and now uv.

Why use uv?

uv, developed by Astral, is designed to do more than resolve and install packages. It also:

  • installs and manages Python interpreters themselves;
  • creates and manages per-project virtual environments automatically;
  • adds and removes a project’s declared dependencies;
  • resolves a consistent, compatible set of versions for the full dependency graph;
  • writes a lockfile recording exactly which versions were resolved;
  • runs commands inside the correct project environment.

Two practical reasons make uv a good default choice. First, it is implemented in Rust and is substantially faster than many of the tools it replaces, particularly for dependency resolution and installation.

Second, it unifies steps that historically required several separate tools, including interpreter installation, environment creation, and dependency locking. This reduces the number of moving parts a beginner needs to keep track of.