pypa/pip · error · KeyError

duplicate labels in project urls

Error message

duplicate labels in project urls

What it means

Raised as KeyError('duplicate labels in project urls') by _parse_project_urls in packaging.metadata when the comma-partitioned Project-URL field contains the same label more than once. Per the spec there is no defined merge behavior, so the field is treated as unparsable.

Source

Thrown at src/pip/_vendor/packaging/metadata.py:216

        #
        # The other potential issue is that it's possible to have the
        # same label multiple times in the metadata, with no solid "right"
        # answer with what to do in that case. As such, we'll do the only
        # thing we can, which is treat the field as unparsable and add it
        # to our list of unparsed fields.
        #
        # TODO: The spec doesn't say anything about if the keys should be
        #       considered case sensitive or not... logically they should
        #       be case-preserving and case-insensitive, but doing that
        #       would open up more cases where we might have duplicate
        #       entries.
        label, _, url = (s.strip() for s in pair.partition(","))

        if label in urls:
            # The label already exists in our set of urls, so this field
            # is unparsable, and we can just add the whole thing to our
            # unparsable data and stop processing it.
            raise KeyError("duplicate labels in project urls")
        urls[label] = url

    return urls


def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
    """Get the body of the message."""
    # If our source is a str, then our caller has managed encodings for us,
    # and we don't need to deal with it.
    if isinstance(source, str):
        payload = msg.get_payload()
        assert isinstance(payload, str)
        return payload
    # If our source is a bytes, then we're managing the encoding and we need
    # to deal with it.
    else:
        bpayload = msg.get_payload(decode=True)
        assert isinstance(bpayload, bytes)

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Dedupe Project-URL entries by label before writing metadata, keeping the intended one.
  2. Validate metadata locally with 'packaging' or twine check before publishing.
  3. When generating, use a dict keyed by label so duplicates cannot occur.
  4. Catch the error during parsing and report which label is duplicated.

Example fix

# before
[Project-URL]
Homepage = \"https://a\"
Homepage = \"https://b\"
# after
[Project-URL]
Homepage = \"https://a\"
Source = \"https://b\"
Defensive patterns

Strategy: validation

Validate before calling

def dedupe_project_urls(pairs):
    seen = set()
    out = []
    for label, url in pairs:
        if label in seen:
            raise ValueError(f'duplicate Project-URL label {label!r}')
        seen.add(label)
        out.append((label, url))
    return out

Try / catch

try:
    md = Metadata.from_email(raw)
except KeyError as e:
    if 'duplicate labels' in str(e):
        # the whole field is unparsable; surface to user
        report_duplicate_url_label()

Prevention

When it happens

Trigger: Metadata with 'Project-URL: Homepage, https://x' and a second 'Project-URL: Homepage, https://y'. Calling parse_email over METADATA containing duplicate labels. Calling _parse_project_urls directly on a raw field string with repeated labels.

Common situations: Generated metadata merging URLs from multiple sources (setup.cfg + pyproject); misconfigured pyproject where 'Homepage' is listed twice; tooling that appends rather than replaces labels.

Related errors


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