pypa/pip · error · UnsupportedWheel
has an invalid wheel
Error message
{name} has an invalid wheel, {e} What it means
Raised by parse_wheel() as a catch-all wrapper: any UnsupportedWheel raised while locating the .dist-info directory, reading the WHEEL metadata, or parsing the Wheel-Version is re-raised prefixed with the wheel name. It signals that the wheel file is structurally invalid and cannot be installed.
Solutions
- Read the full error message — the text after 'has an invalid wheel,' identifies the specific sub-failure (dist-info, metadata, or version)
- Rebuild the wheel with a current build tool: 'python -m build --wheel'
- Re-download the wheel from the official index to rule out a truncated/corrupt download
- Upgrade pip to the latest version in case the wheel uses a newer but compatible format
- Inspect the wheel zip contents: 'unzip -l <wheel>' and verify a .dist-info/WHEEL file exists
Example fix
// before pip install ./custom_built-1.0.whl // after # rebuild with a standard, up-to-date toolchain python -m pip install --upgrade build python -m build --wheel pip install dist/custom_built-1.0-py3-none-any.whl
Defensive patterns
Strategy: try-catch
Validate before calling
import zipfile
from zipimport import ZipImportError
def is_valid_wheel(path: str) -> bool:
if not path.endswith('.whl'):
return False
try:
zf = zipfile.ZipFile(path)
except (zipfile.BadZipFile, FileNotFoundError):
return False
names = zf.namelist()
dirs = {n.split('/')[0] for n in names if '/' in n}
info_dirs = [d for d in dirs if d.endswith('.dist-info')]
if len(info_dirs) != 1:
return False
has_wheel = any(n.endswith('.dist-info/WHEEL') for n in names)
return has_wheel Type guard
import zipfile
def is_installable_wheel(path: str) -> bool:
try:
with zipfile.ZipFile(path) as zf:
return any(n.split('/')[0].endswith('.dist-info') for n in zf.namelist() if '/' in n)
except (zipfile.BadZipFile, FileNotFoundError):
return False Try / catch
from pip._internal.exceptions import UnsupportedWheel
try:
parse_wheel(wheel_zip, name)
except UnsupportedWheel as e:
# the detail after 'has an invalid wheel,' tells you the specific issue
raise PackagingError(f'wheel {name} is malformed: {e}') Prevention
- Build wheels with standard backends (setuptools, hatchling, flit)
- Verify wheels with 'unzip -l <file>.whl' before publishing or installing
- Re-download wheels from the official index if downloads may be corrupt
- Upgrade pip to handle newer wheel formats
When it happens
Trigger: Calling parse_wheel(wheel_zip, name) where the zip fails one of the inner validation steps: wheel_dist_info_dir(), wheel_metadata(), or wheel_version(). The original UnsupportedWheel detail is appended after 'has an invalid wheel,'.
Common situations: A corrupt or hand-built wheel missing the .dist-info directory or WHEEL file. A wheel built by a very old or non-standard build tool. A truncated download producing an incomplete zip. A wheel whose metadata version pip cannot parse.
Related errors
- .dist-info directory
- .dist-info directory not found
- invalid Wheel-Version
- multiple .dist-info directories found
- WHEEL is missing Wheel-Version
AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08).
Data as JSON: /api/errors/28e3b487ccf5c36b.
Report an issue: GitHub.
Appendix: source
Thrown at src/pip/_internal/utils/wheel.py:29
VERSION_COMPATIBLE = (1, 0)
logger = logging.getLogger(__name__)
def parse_wheel(wheel_zip: ZipFile, name: str) -> tuple[str, Message]:
"""Extract information from the provided wheel, ensuring it meets basic
standards.
Returns the name of the .dist-info directory and the parsed WHEEL metadata.
"""
try:
info_dir = wheel_dist_info_dir(wheel_zip, name)
metadata = wheel_metadata(wheel_zip, info_dir)
version = wheel_version(metadata)
except UnsupportedWheel as e:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
check_compatibility(version, name)
return info_dir, metadata
def wheel_dist_info_dir(source: ZipFile, name: str) -> str:
"""Returns the name of the contained .dist-info directory.
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
it doesn't match the provided name.
"""
# Zip file path separators must be /
subdirs = {p.split("/", 1)[0] for p in source.namelist()}
info_dirs = [s for s in subdirs if s.endswith(".dist-info")]
if not info_dirs:View on GitHub (pinned to f399c37189)