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

  1. Read the full error message — the text after 'has an invalid wheel,' identifies the specific sub-failure (dist-info, metadata, or version)
  2. Rebuild the wheel with a current build tool: 'python -m build --wheel'
  3. Re-download the wheel from the official index to rule out a truncated/corrupt download
  4. Upgrade pip to the latest version in case the wheel uses a newer but compatible format
  5. 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

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


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)