{"id":"b08b1f0258e7184b","repo":"pypa/pip","slug":"invalid-wheel-filename-extension-must-be-whl","errorCode":null,"errorMessage":"Invalid wheel filename (extension must be '.whl'): {filename!r}","messagePattern":"Invalid wheel filename \\(extension must be '\\.whl'\\): (.+?)","errorType":"validation","errorClass":"InvalidWheelFilename","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/packaging/utils.py","lineNumber":203,"sourceCode":"\n    >>> from packaging.utils import parse_wheel_filename\n    >>> from packaging.tags import Tag\n    >>> from packaging.version import Version\n    >>> name, ver, build, tags = parse_wheel_filename(\"foo-1.0-py3-none-any.whl\")\n    >>> name\n    'foo'\n    >>> ver == Version('1.0')\n    True\n    >>> tags == {Tag(\"py3\", \"none\", \"any\")}\n    True\n    >>> not build\n    True\n\n    .. versionadded:: 26.1\n       The *validate_order* parameter.\n    \"\"\"\n    if not filename.endswith(\".whl\"):\n        raise InvalidWheelFilename(\n            f\"Invalid wheel filename (extension must be '.whl'): {filename!r}\"\n        )\n\n    filename = filename[:-4]\n    dashes = filename.count(\"-\")\n    if dashes not in (4, 5):\n        raise InvalidWheelFilename(\n            f\"Invalid wheel filename (wrong number of parts): {filename!r}\"\n        )\n\n    parts = filename.split(\"-\", dashes - 2)\n    name_part = parts[0]\n    # See PEP 427 for the rules on escaping the project name.\n    if \"__\" in name_part or re.match(r\"^[\\w\\d._]*$\", name_part, re.UNICODE) is None:\n        raise InvalidWheelFilename(f\"Invalid project name: {filename!r}\")\n    name = canonicalize_name(name_part)\n\n    try:","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/packaging/utils.py#L185-L221","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","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`."],"exampleFix":"// before\nname, ver, build, tags = parse_wheel_filename(archive)  # raises for 'foo-1.0.tar.gz'\n\n# after\nif archive.endswith('.whl'):\n    name, ver, build, tags = parse_wheel_filename(archive)\nelif archive.endswith(('.tar.gz', '.zip')):\n    name, ver = parse_sdist_filename(archive)\nelse:\n    raise ValueError(f\"unknown archive type: {archive!r}\")","handlingStrategy":"validation","validationCode":"def is_wheel_filename(filename: str) -> bool:\n    return filename.endswith('.whl')","typeGuard":"def is_wheel_filename(name: object) -> bool:\n    return isinstance(name, str) and name.endswith('.whl')","tryCatchPattern":"from pip._vendor.packaging.utils import parse_wheel_filename, InvalidWheelFilename\n\nif filename.endswith('.whl'):\n    try:\n        name, ver, build, tags = parse_wheel_filename(filename)\n    except InvalidWheelFilename:\n        skip(filename)\nelse:\n    # route to sdist parser or skip\n    pass","preventionTips":["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."],"tags":["python","packaging","utils","wheel","pep427","filename"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}