pypa/pip · error · InstallationError

Multiple locked links provided for

Error message

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

What it means

Raised by CandidateEvaluator.add_locked_link (package_finder.py:1103) when a second locked link is registered for the same canonical project name. Locks must be unique per project, so a duplicate is treated as an inconsistent resolution set rather than silently overwriting the first.

Solutions

  1. De-duplicate the lock/resolution input so each canonical project name appears exactly once.
  2. Canonicalize names (pip._vendor.packaging.utils.canonicalize_name) before building the locked-link set to catch alias collisions early.
  3. If two entries are intentional, decide which link wins and remove the other.

Example fix

// before
flask==3.0.0
Flask==2.3.3
// after (single canonical entry)
flask==3.0.0
Defensive patterns

Strategy: validation

Validate before calling

// Canonicalize and de-duplicate locked entries before building the lock set:
from pip._vendor.packaging.utils import canonicalize_name
seen = {}
for name, link in locked_entries:
    cn = canonicalize_name(name)
    if cn in seen:
        raise SystemExit(f'duplicate locked entry for {cn}: {seen[cn]} and {link}')
    seen[cn] = link

Try / catch

from pip._internal.exceptions import InstallationError
try:
    finder.add_locked_link(project_name, locked_link)
except InstallationError as e:
    if 'Multiple locked links' in str(e):
        # resolve the duplicate before retrying
        ...

Prevention

When it happens

Trigger: Calling add_locked_link twice for the same project_name (after the first has been stored in self._locked_links), or feeding a lock/resolution file that contains two distinct entries resolving to the same canonicalized project name.

Common situations: A requirements/lock file that lists the same package twice under different aliases/casings that canonicalize to one name (e.g. 'Flask' and 'flask'); merging two lock files naively; a tool that emits duplicate locked entries.

Related errors


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

Appendix: 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 f399c37189)