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

  1. Route by extension first: use `parse_wheel_filename` only when `filename.endswith('.whl')`, else `parse_sdist_filename`.
  2. Verify the file is actually a wheel (the artifact should be a ZIP archive with a `.whl` extension).
  3. Fix the filename if it was mangled by an upstream download/copy step.
  4. 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

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


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