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
- De-duplicate the lock/resolution input so each canonical project name appears exactly once.
- Canonicalize names (pip._vendor.packaging.utils.canonicalize_name) before building the locked-link set to catch alias collisions early.
- 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
- Canonicalize all project names before assembling a lock file.
- Run a uniqueness check over canonical names when merging lock files.
- Treat duplicate locked entries as a build error in your lock-generation tool.
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
- Could not install locked package
- No matching distribution found for
- Cannot determine archive format of
- Cannot select requirements from pylock file
- Cannot set --home and --prefix together
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)