pypa/pip · error · InvalidWheelFilename
Invalid wheel filename (extension must be '.whl'): {filename
Error message
Invalid wheel filename (extension must be '.whl'): {filename!r} What it means
Raised by `parse_wheel_filename(filename)` when the filename does not end with `.whl`. Wheels must follow PEP 427 (`{name}-{version}(-{build})?-{py}-{abi}-{platform}.whl`); the extension is the first structural check the parser performs, before counting dashes or splitting parts.
Source
Thrown at src/pip/_vendor/packaging/utils.py:203
>>> from packaging.utils import parse_wheel_filename
>>> from packaging.tags import Tag
>>> from packaging.version import Version
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
>>> name
'foo'
>>> ver == Version('1.0')
True
>>> tags == {Tag("py3", "none", "any")}
True
>>> not build
True
.. versionadded:: 26.1
The *validate_order* parameter.
"""
if not filename.endswith(".whl"):
raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
)
filename = filename[:-4]
dashes = filename.count("-")
if dashes not in (4, 5):
raise InvalidWheelFilename(
f"Invalid wheel filename (wrong number of parts): {filename!r}"
)
parts = filename.split("-", dashes - 2)
name_part = parts[0]
# See PEP 427 for the rules on escaping the project name.
if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
name = canonicalize_name(name_part)
try:View on GitHub (pinned to d7d0d0a394)
Solutions
- Route by extension first: use `parse_wheel_filename` only when `filename.endswith('.whl')`, else `parse_sdist_filename`.
- Verify the file is actually a wheel (the artifact should be a ZIP archive with a `.whl` extension).
- Fix the filename if it was mangled by an upstream download/copy step.
- Guard with a pre-check `if not filename.endswith('.whl'): continue`.
Example fix
// before
name, ver, build, tags = parse_wheel_filename(archive) # raises for 'foo-1.0.tar.gz'
# after
if archive.endswith('.whl'):
name, ver, build, tags = parse_wheel_filename(archive)
elif archive.endswith(('.tar.gz', '.zip')):
name, ver = parse_sdist_filename(archive)
else:
raise ValueError(f"unknown archive type: {archive!r}") Defensive patterns
Strategy: validation
Validate before calling
def is_wheel_filename(filename: str) -> bool:
return filename.endswith('.whl') Type guard
def is_wheel_filename(name: object) -> bool:
return isinstance(name, str) and name.endswith('.whl') Try / catch
from pip._vendor.packaging.utils import parse_wheel_filename, InvalidWheelFilename
if filename.endswith('.whl'):
try:
name, ver, build, tags = parse_wheel_filename(filename)
except InvalidWheelFilename:
skip(filename)
else:
# route to sdist parser or skip
pass Prevention
- Dispatch by extension before parsing: `.whl` -> wheel, `.tar.gz`/`.zip` -> sdist.
- Treat the extension as part of the file contract; never strip it.
- Validate archive integrity (ZIP magic for wheels) before parsing the name.
When it happens
Trigger: Passing a `.tar.gz`/`.zip` sdist name, a raw project name, a `.egg`/`.tar.bz2`, or any path lacking the `.whl` suffix into `parse_wheel_filename`.
Common situations: Iterating a mixed directory of wheels and sdists and dispatching all filenames through the wheel parser; misconfigured build artifacts; copy-paste from a sdist code path; case/path issues where the extension was stripped.
Related errors
- Invalid wheel filename (wrong number of parts): {filename!r}
- Invalid project name: {filename!r}
- Invalid build number: {build_part} in {filename!r}
- Invalid wheel filename (invalid version): {filename!r}
- Invalid wheel filename (compressed tag set components must b
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/b08b1f0258e7184b.json.
Report an issue: GitHub.