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
- If pinning to source, provide exactly one of vcs, directory, or archive.
- If pinning to artifacts, ensure sdist or wheels is populated (which makes this branch not fire).
- Regenerate the lockfile with a resolver that always records one resolution.
- 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 pinning to source, set exactly one of vcs/directory/archive.
- When pinning to artifacts, populate sdist or wheels.
- Validate the count of source locators before loading.
- Use a resolver that always records one resolution per package.
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
- None of vcs, directory, archive must be set if sdist or whee
- path or url must be provided
- Cannot determine sdist filename
- Cannot determine wheel filename
- Invalid wheel filename {wheel.filename!r}
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/0fef78e18fd342ec.json.
Report an issue: GitHub.