pypa/pip · error · PylockValidationError

None of vcs, directory, archive must be set if sdist or whee

Error message

None of vcs, directory, archive must be set if sdist or wheels are set

What it means

Raised as PylockValidationError by Package._from_dict in packaging.pylock when a package entry specifies both a built distribution (sdist and/or wheels) and a direct source locator (vcs, directory, or archive). pylock treats these as mutually exclusive: a package is either pinned to artifacts or pinned to a source, not both.

Source

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

            version=_get_as(d, str, Version, "version"),
            requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
            dependencies=_get_sequence(d, Mapping, "dependencies"),  # type: ignore[type-abstract]
            marker=_get_as(d, str, Marker, "marker"),
            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 "

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Decide whether the package is artifact-pinned or source-pinned, then keep only that side.
  2. Remove the vcs/directory/archive block when sdist/wheels are present, or vice versa.
  3. Regenerate the lockfile with a single resolution strategy per package.
  4. Split into two package entries if both forms are genuinely needed (note: pylock forbids this; reconsider).

Example fix

# before
[packages]
name = \"foo\"
wheels = [{ name = \"foo-1.0.whl\", hashes = {...} }]
vcs = { url = \"https://github.com/x/foo\", revision = \"v1.0\" }
# after
[packages]
name = \"foo\"
wheels = [{ name = \"foo-1.0.whl\", hashes = {...} }]
Defensive patterns

Strategy: validation

Validate before calling

def is_exclusive_resolution(pkg: dict) -> bool:
    has_dist = bool(pkg.get('sdist')) or bool(pkg.get('wheels'))
    has_src = bool(pkg.get('vcs')) or bool(pkg.get('directory')) or bool(pkg.get('archive'))
    return not (has_dist and has_src)

Type guard

def has_no_dist_source_mix(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 not (dists > 0 and srcs > 0)

Try / catch

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

Prevention

When it happens

Trigger: A [[packages]] table with both sdist = {...} (or wheels = [...]) and vcs = {...}; an entry with directory and wheels; an entry combining archive with sdist. The check counts distributions = bool(sdist)+len(wheels) and direct_urls = bool(vcs)+bool(directory)+bool(archive) and fires when both > 0.

Common situations: Merging two lockfile fragments (one pinned to artifacts, one to a Git URL) for the same package; hand-editing a lockfile to add a vcs override to an already-pinned package; tooling that emits both forms.

Related errors


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