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 by Package._from_dict in pylock.py:608-612. When a package has no sdist and no wheels, it MUST declare exactly one direct-URL source among vcs/directory/archive. Zero sources (no way to fetch) or more than one (ambiguous) raises PylockValidationError.

Solutions

  1. Add exactly one of vcs/directory/archive to the package entry.
  2. If two are present, remove one so only a single source remains.

Example fix

# before
[[packages]]
name = "x"
version = "1.0"
# after
[[packages]]
name = "x"
version = "1.0"
  [packages.vcs]
  type = "git"
  url = "https://example.com/x.git"
  commit-id = "abc123"
Defensive patterns

Strategy: validation

Validate before calling

def package_has_exactly_one_source(pkg) -> bool:
    distributions = bool(pkg.get("sdist")) + len(pkg.get("wheels") or [])
    direct = bool(pkg.get("vcs")) + bool(pkg.get("directory")) + bool(pkg.get("archive"))
    if distributions > 0:
        return True
    return direct == 1

Try / catch

from packaging.pylock import Pylock, PylockValidationError

try:
    Pylock.from_dict(d)
except PylockValidationError as e:
    ...

Prevention

When it happens

Trigger: A [[packages]] entry with only name/index and no vcs, directory, archive, sdist or wheels; or a package declaring both vcs and directory (two sources).

Common situations: Forgetting the source entirely for a direct-reference package; providing two sources for redundancy; assuming 'index' counts as a source (it does not for this check).

Related errors


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

Appendix: source

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

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