{"id":"9c558818c12188a6","repo":"pypa/pip","slug":"multiple-dist-info-directories-found","errorCode":null,"errorMessage":"multiple .dist-info directories found: {}","messagePattern":"multiple \\.dist-info directories found: (.+?)","errorType":"exception","errorClass":"UnsupportedWheel","httpStatus":null,"severity":"error","filePath":"src/pip/_internal/utils/wheel.py","lineNumber":51,"sourceCode":"    return info_dir, metadata\n\n\ndef wheel_dist_info_dir(source: ZipFile, name: str) -> str:\n    \"\"\"Returns the name of the contained .dist-info directory.\n\n    Raises AssertionError or UnsupportedWheel if not found, >1 found, or\n    it doesn't match the provided name.\n    \"\"\"\n    # Zip file path separators must be /\n    subdirs = {p.split(\"/\", 1)[0] for p in source.namelist()}\n\n    info_dirs = [s for s in subdirs if s.endswith(\".dist-info\")]\n\n    if not info_dirs:\n        raise UnsupportedWheel(\".dist-info directory not found\")\n\n    if len(info_dirs) > 1:\n        raise UnsupportedWheel(\n            \"multiple .dist-info directories found: {}\".format(\", \".join(info_dirs))\n        )\n\n    info_dir = info_dirs[0]\n\n    info_dir_name = canonicalize_name(info_dir)\n    canonical_name = canonicalize_name(name)\n    if not info_dir_name.startswith(canonical_name):\n        raise UnsupportedWheel(\n            f\".dist-info directory {info_dir!r} does not start with {canonical_name!r}\"\n        )\n\n    return info_dir\n\n\ndef read_wheel_metadata_file(source: ZipFile, path: str) -> bytes:\n    try:\n        return source.read(path)","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_internal/utils/wheel.py#L33-L69","documentation":"UnsupportedWheel raised when the wheel zip contains more than one top-level `.dist-info` directory. The wheel spec mandates exactly one; multiple indicate a malformed build (often from concatenating or merging wheels, or a backend bug emitting metadata twice). The error lists all offending directory names.","triggerScenarios":"wheel_dist_info_dir on a zip where two or more top-level entries end in .dist-info. Happens when zipping two wheels together, double-running the metadata generation step, or a backend that writes metadata for both an old and new build into one archive.","commonSituations":"Custom build script that appends to an existing wheel; incremental build leftover; tool that merges debug+release metadata.","solutions":["Inspect: `unzip -l pkg.whl | grep dist-info` and identify the duplicates.","Rebuild from a clean dist/ directory: `rm -rf dist build *.egg-info && python -m build --wheel`.","If merging artifacts, keep only one .dist-info per wheel.","Verify with `twine check` after rebuild."],"exampleFix":"// before\n# stale dist/ from previous build concatenated\npython -m build --wheel\n\n// after\nrm -rf dist build *.egg-info && python -m build --wheel && twine check dist/*","handlingStrategy":"validation","validationCode":"import zipfile\ndef exactly_one_dist_info(path: str) -> bool:\n    with zipfile.ZipFile(path) as z:\n        info_dirs = {n.split('/',1)[0] for n in z.namelist() if n.split('/',1)[0].endswith('.dist-info')}\n        return len(info_dirs) == 1","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Build from a clean dist/ each time: `rm -rf dist build *.egg-info`.","Use a PEP 517 backend.","Run `twine check` to catch duplicate metadata.","Never merge wheels together."],"tags":["pip","wheel","metadata","dist-info","validation"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}