pypa/pip · error · IDNAError

Label must not start or end with a hyphen

Error message

Label must not start or end with a hyphen

What it means

IDNAError from check_hyphen_ok: per RFC 5891 a label must neither start nor end with a hyphen ('-'). Leading/trailing hyphens are reserved shapes conflicts (and historically used for wildcard/underscore-style abuse) and break DNS interoperability.

Source

Thrown at src/pip/_vendor/idna/core.py:203

        raise IDNAError("Label begins with an illegal combining character")
    return True


def check_hyphen_ok(label: str) -> bool:
    """Validate the hyphen restrictions for a label.

    Per :rfc:`5891` §4.2.3.1 a label must not start or end with a hyphen
    (``U+002D``), and must not have hyphens in both the third and fourth
    positions (the prefix reserved for A-labels).

    :param label: The label to check.
    :returns: ``True`` if the hyphen restrictions are satisfied.
    :raises IDNAError: If any of the hyphen restrictions are violated.
    """
    if label[2:4] == "--":
        raise IDNAError("Label has disallowed hyphens in 3rd and 4th position")
    if label[0] == "-" or label[-1] == "-":
        raise IDNAError("Label must not start or end with a hyphen")
    return True


def check_nfc(label: str) -> None:
    """Require that a label is in Unicode Normalization Form C.

    :param label: The label to check.
    :raises IDNAError: If ``label`` differs from its NFC normalisation.
    """
    if len(label) > _max_input_length:
        raise IDNAError("Label too long")
    if unicodedata.normalize("NFC", label) != label:
        raise IDNAError("Label must be in Normalization Form C")


def valid_contextj(label: str, pos: int) -> bool:
    """Validate the CONTEXTJ rules from :rfc:`5892` Appendix A.

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Strip leading/trailing hyphens from each label before encoding: label.strip('-').
  2. Fix the slugify/sanitizer to never emit a leading or trailing hyphen.
  3. Treat empty labels (after stripping) as invalid and skip them.

Example fix

# before
idna.encode('-العرب-')  # must not start or end with a hyphen

# after
idna.encode('العرب'.strip('-'))
Defensive patterns

Strategy: validation

Validate before calling

def trim_hyphen_edges(label: str) -> str:
    return label.strip('-')

Type guard

def has_no_edge_hyphens(label: str) -> bool:
    return bool(label) and label[0] != '-' and label[-1] != '-'

Try / catch

from idna import IDNAError
try:
    idna.encode(label)
except IDNAError as e:
    if 'start or end with a hyphen' in str(e):
        label = label.strip('-')
    else:
        raise

Prevention

When it happens

Trigger: A label like '-example', 'example-', or '-' being passed through alabel/check_label. Most commonly the trailing case, where a slugify routine appended a hyphen that was never trimmed.

Common situations: slugify() output not stripped of leading/trailing '-'; concatenation 'prefix-' + '' leaving a dangling hyphen; user input normalized to lowercase-then-hyphenated without trimming.

Related errors


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