November 30, 2020·6 min read·engineeringpythonpipdependencies

pip 20.3 makes incompatible dependency sets fail installation

pip's new resolver becomes the default on Python 3. Backtracking can find compatible versions, while stricter constraint handling exposes conflicts that the old installer could accept.

An installer can finish without having found a compatible set of packages. The earlier pip resolver allowed that outcome in cases where requirements conflicted. With pip 20.3, released November 30, the new dependency resolver becomes the default for Python 3. It can revisit candidate versions as it learns their dependencies, and it refuses combinations that cannot satisfy the requirements being resolved.

This changes the meaning of some previously successful installations. A command that depended on pip overlooking a conflict may now fail. That failure can be useful information about the requested environment. It can also expose incorrect dependency metadata, so a rejection needs to be interpreted in terms of the requirements that produced it.

I dislike how much packaging work Python can demand before useful code runs. This change addresses a specific weakness in that machinery. An installer should not report a compatible result when the declarations it has been given contradict one another.

The documented conflict has no acceptable version

The migration guide shipped with pip 20.3 gives a concrete example involving six and virtualenv. It says the old resolver can install six==1.11 when asked for six<1.12 together with virtualenv==20.0.2, despite that virtualenv release declaring six>=1.12.0,<2.

The new resolver rejects that request. The two permitted intervals for six do not overlap. Selecting another candidate within either interval cannot satisfy the other requirement. A successful installation in this case would not demonstrate successful dependency resolution, even if some operations happened to run.

The guide's example can be expressed as this command. It is an illustration of a conflicting request, not a command expected to install successfully under the new resolver.

python -m pip install "six<1.12" "virtualenv==20.0.2"

That distinction matters when comparing installer versions. An old command's zero exit status is evidence about the old command's behaviour. It is not proof that its chosen package versions satisfied all declared dependencies. The new error can reveal a conflict that already existed in the requested set.

Backtracking revisits assumptions about versions

The versioned user guide explains why pip may download several versions of the same package during one install. Resolution begins with candidate choices before every relevant dependency is known. When a later requirement invalidates an earlier choice, the resolver can discard that choice and try another version.

Backtracking therefore performs work that the old behaviour could avoid by accepting an inconsistent outcome. Its purpose is to find versions that work together under the declared constraints. The process can finish with a compatible set or with an error saying that resolution is impossible.

The guide connects some of the cost to metadata availability. Package indexes do not supply all dependency information in advance of downloading packages. Obtaining another candidate can be part of learning whether that candidate is compatible. Repeated download messages can consequently describe a search through alternatives, rather than repeated installation of the same package.

The tagged release notes record changes to backtracking and state management intended to avoid an infinite loop. They also record deterministic resolution order and less duplicated information in conflict messages. These are implementation and diagnostic improvements around the search, not evidence that every dependency graph can be resolved quickly.

More search is a real cost

The guide says that backtracking can take substantial time and that pip does not know in advance how much computation a particular search will need. A package with many candidate versions can require repeated investigation. The new resolver's stricter result has an operational cost when the requested graph leaves many alternatives open.

Constraining candidate versions can reduce that work. The guide recommends focusing on the package whose versions are being revisited. A lower bound can remove old candidates from consideration. An exact version can make a choice explicit, provided that the chosen version still satisfies the other requirements.

Those restrictions can also exclude the only compatible candidate. A constraint accelerates the search by changing the permitted inputs. It cannot turn incompatible requirements into compatible ones. That tradeoff is especially relevant when a constraint is added solely to make a slow resolution finish sooner.

For repeated deployments, the guide discusses resolving a specific package set during development and recording exact versions for later installation. That moves the search work to a point where its result can be examined and reused. It does not establish that every future change to that set can reuse the same answer.

I prefer to distinguish resolution work from installation work when reading these logs. Downloading another candidate can be necessary evidence gathering. It is still a cost that deserves explanation, especially when an application build depends on completing the same search repeatedly.

Constraints narrow choices instead of overriding requirements

The migration guide describes constraints files as global version limits. They restrict the candidate versions visible to the resolver. They do not override the requirements declared by another package. That removes a possible misunderstanding about what a pin in a constraints file can accomplish.

Suppose a package requires a dependency version below a particular boundary while the application's constraints permit only versions above it. The constraints do not instruct pip to disregard the package requirement. They leave no acceptable candidate. Changing the file may change what is permitted, but it does not alter the package's declaration.

Editable installations also remain subject to version requirements and constraints. The guide explicitly says that -e . does not cause those restrictions to be ignored. A local checkout with an incompatible declared version can therefore conflict with a pinned requirement, even when its source tree is available.

The documented constraints format becomes narrower in other ways. Constraints must be named requirements, cannot specify links, and cannot request extras. Those restrictions help distinguish limiting the version of an existing dependency from requesting a new installation behaviour. Workflows that used the older, broader interpretation can encounter errors on migration.

The guide also warns about combining constraints with hash-checking mode, which requires exact version matches for requirements. Version limits and artifact hashes impose different restrictions, and a workflow using both needs compatible declarations for each.

One invocation does not validate an entire environment forever

The most important limit in the migration guide is its warning about packages already installed. pip resolves the packages involved in the current command. It does not guarantee that the entire environment remains consistent after any sequence of separate installation commands.

The guide explains that installing one package and then another can produce a different result from requesting both in one invocation. A subsequent installation may break an already installed package's requirements. The new resolver's refusal to construct an inconsistent requested set should therefore not be described as a universal environment-consistency guarantee.

This limit changes how the stronger behaviour should be evaluated. The question is which requirements participated in a resolution, not simply whether the environment has ever been touched by pip 20.3. An environment assembled incrementally has a history that one successful command cannot validate in full.

The guide recommends pip check to report inconsistencies among installed packages. That is a separate check against the current environment. Its role remains useful because resolving a command and inspecting the resulting installed set answer related but distinct questions.

Metadata errors need metadata repairs

A resolver operates on dependency declarations. If those declarations are wrong, the error does not establish that the corresponding code paths are incompatible in every possible execution. It establishes that pip cannot satisfy the supplied metadata and requirements together.

The migration guide discusses temporary workarounds, including --no-deps when a user deliberately manages dependencies despite conflicting metadata. That option disables dependency resolution. It should not be presented as making the new resolver find a valid solution. The underlying declarations still need correction if the package set is to be resolved normally.

pip 20.3 also retains an explicit --use-deprecated=legacy-resolver escape hatch during the transition. Python 2 continues to use the legacy resolver by default. The default change discussed here applies to Python 3, so the interpreter running pip remains part of the observed behaviour.

The useful outcome is a more precise installation decision. Compatible candidates can be found through backtracking, and an impossible requested combination produces a refusal instead of an apparently successful inconsistent installation. Existing environments still need their own consistency checks, while constraints and package metadata determine which candidate combinations the resolver is allowed to accept.