pypa/pip · error · PylockValidationError

Exactly one of vcs, directory, archive must be set if sdist

Error message

Exactly one of vcs, directory, archive must be set if sdist and wheels are not set

What it means

Raised as PylockValidationError by Package._from_dict in packaging.pylock when a package entry has no built distributions (no sdist and no wheels) AND does not have exactly one direct source locator. Without artifacts, exactly one of vcs/directory/archive must pin the source so resolution is unambiguous.

Source

Thrown at src/pip/_vendor/packaging/pylock.py:593

            vcs=_get_object(d, PackageVcs, "vcs"),
            directory=_get_object(d, PackageDirectory, "directory"),
            archive=_get_object(d, PackageArchive, "archive"),
            index=_get(d, str, "index"),
            sdist=_get_object(d, PackageSdist, "sdist"),
            wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
            attestation_identities=_get_sequence(d, Mapping, "attestation-identities"),  # type: ignore[type-abstract]
            tool=_get(d, Mapping, "tool"),  # type: ignore[type-abstract]
        )
        distributions = bool(package.sdist) + len(package.wheels or [])
        direct_urls = (
            bool(package.vcs) + bool(package.directory) + bool(package.archive)
        )
        if distributions > 0 and direct_urls > 0:
            raise PylockValidationError(
                "None of vcs, directory, archive must be set if sdist or wheels are set"
            )
        if distributions == 0 and direct_urls != 1:
            raise PylockValidationError(
                "Exactly one of vcs, directory, archive must be set "
                "if sdist and wheels are not set"
            )
        for i, wheel in enumerate(package.wheels or []):
            try:
                (name, version, _, _) = parse_wheel_filename(wheel.filename)
            except Exception as e:
                raise PylockValidationError(
                    f"Invalid wheel filename {wheel.filename!r}",
                    context=f"wheels[{i}]",
                ) from e
            if name != package.name:
                raise PylockValidationError(
                    f"Name in {wheel.filename!r} is not consistent with "
                    f"package name {package.name!r}",
                    context=f"wheels[{i}]",
                )
            if package.version and version != package.version:

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. If pinning to source, provide exactly one of vcs, directory, or archive.
  2. If pinning to artifacts, ensure sdist or wheels is populated (which makes this branch not fire).
  3. Regenerate the lockfile with a resolver that always records one resolution.
  4. Remove the conflicting extra locator so only one remains.

Example fix

# before
[packages]
name = \"foo\"
vcs = { url = \"https://github.com/x/foo\" }
directory = \"./foo\"
# after
[packages]
name = \"foo\"
directory = \"./foo\"
Defensive patterns

Strategy: validation

Validate before calling

def has_exactly_one_source(pkg: dict) -> bool:
    has_dist = bool(pkg.get('sdist')) or bool(pkg.get('wheels'))
    srcs = bool(pkg.get('vcs')) + bool(pkg.get('directory')) + bool(pkg.get('archive'))
    return has_dist or srcs == 1

Type guard

def is_valid_source_count(pkg: dict) -> bool:
    dists = bool(pkg.get('sdist')) + len(pkg.get('wheels') or [])
    srcs = bool(pkg.get('vcs')) + bool(pkg.get('directory')) + bool(pkg.get('archive'))
    return dists > 0 or srcs == 1

Try / catch

try:
    PylockFile.from_dict(data)
except PylockValidationError as e:
    if 'Exactly one of vcs, directory, archive' in str(e):
        keep_single_source_locator(e.context)

Prevention

When it happens

Trigger: A [[packages]] table with neither sdist nor wheels and zero source locators (nothing to install), or with two/three source locators (e.g. both vcs and directory). The check: distributions == 0 and direct_urls != 1.

Common situations: Hand-editing a lockfile and deleting the artifact block without adding a source; specifying both a Git URL and a local directory for the same package; a resolver that emits a package with no resolvable source.

Related errors


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