pypa/pip · error · InstallationError

Multiple locked links provided for {project_name}: {self._lo

Error message

Multiple locked links provided for {project_name}: {self._locked_links[project_name]} and {locked_link}

What it means

InstallationError raised in add_locked_link when a second locked link is registered for a project name that already has one. The locked-link table must map one project to exactly one artifact; a duplicate means the lockfile/constraints source is internally inconsistent.

Source

Thrown at src/pip/_internal/index/package_finder.py:1103

            logger.debug(
                "Using version %s (newest of versions: %s)",
                best_candidate.version,
                _format_versions(best_candidate_result.applicable_candidates),
            )
            return best_candidate

        # We have an existing version, and its the best version
        logger.debug(
            "Installed version (%s) is most up-to-date (past versions: %s)",
            installed_version,
            _format_versions(best_candidate_result.applicable_candidates),
        )
        raise BestVersionAlreadyInstalled

    def add_locked_link(self, project_name: NormalizedName, locked_link: Link) -> None:
        assert not self._all_candidates
        if project_name in self._locked_links:
            raise InstallationError(
                f"Multiple locked links provided for {project_name}: "
                f"{self._locked_links[project_name]} and {locked_link}"
            )

        self._locked_links[project_name] = locked_link


def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
    """Find the separator's index based on the package's canonical name.

    :param fragment: A <package>+<version> filename "fragment" (stem) or
        egg fragment.
    :param canonical_name: The package's canonical name.

    This function is needed since the canonicalized name does not necessarily
    have the same length as the egg info's name part. An example::

    >>> fragment = 'foo__bar-1.0'

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Inspect the lockfile/constraints file for duplicate entries for the named project (account for name normalization - 'Foo', 'foo', 'F-O-O' all canonicalize together).
  2. Keep a single pinned artifact per project in the lock and remove the duplicate.
  3. Regenerate the lockfile with a current lockfile tool to deduplicate automatically.
  4. If merging locks, resolve the conflict explicitly before installing.

Example fix

# before - locked-requirements.txt
foo==1.0 --hash=sha:aaa
Foo==1.1 --hash=sha:bbb   # duplicate after canonicalization

# after
foo==1.0 --hash=sha:aaa
Defensive patterns

Strategy: validation

Validate before calling

from pip._internal.utils.misc import canonicalize_name
seen = {}
for project, link in locked_links:
    c = canonicalize_name(project)
    if c in seen:
        print(f'duplicate lock entry for {c}: {seen[c]} vs {link}')
    seen[c] = link

Type guard

def no_duplicate_locks(locked_links) -> bool:
    from pip._internal.utils.misc import canonicalize_name
    names = [canonicalize_name(p) for p, _ in locked_links]
    return len(names) == len(set(names))

Try / catch

from pip._internal.exceptions import InstallationError
try:
    finder.add_locked_link(name, link)
except InstallationError as e:
    if 'Multiple locked links' in str(e):
        # deduplicate the lockfile by canonical name
        ...

Prevention

When it happens

Trigger: Triggered when code (lockfile loader / resolver) calls finder.add_locked_link(project_name, link) twice for the same canonical project_name with different Link objects. self._all_candidates is asserted empty, so it happens during the setup phase before any resolution.

Common situations: A lockfile or constraints file that pins two different files/URLs for the same package; a bug in a lockfile generator producing duplicate entries under different canonicalizations (e.g. 'Foo' and 'foo'); merging two lockfiles that both pin the same dependency differently.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/3f3f7751e47b8e4d.json. Report an issue: GitHub.