pypa/pip · error · IDNAError

Label must be in Normalization Form C

Error message

Label must be in Normalization Form C

What it means

IDNAError from check_nfc: a label that is not in Unicode Normalization Form C is rejected. RFC 5891 requires U-labels to be in NFC so that visually identical sequences have a single canonical form, preventing homograph ambiguity. The check compares unicodedata.normalize('NFC', label) to the original.

Source

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

    :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.

    These rules govern the contextual use of the joiner codepoints
    ``U+200C`` (ZERO WIDTH NON-JOINER, Appendix A.1) and ``U+200D``
    (ZERO WIDTH JOINER, Appendix A.2) within a label.

    :param label: The label containing the codepoint.
    :param pos: Index of the joiner codepoint within ``label``.
    :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTJ
        rule, ``False`` otherwise (including when the codepoint at
        ``pos`` is not a recognised joiner).
    :raises ValueError: If an adjacent codepoint has no Unicode name when
        determining its combining class.
    :raises IDNAError: If ``label`` exceeds the defensive input length limit.
    """

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Normalize the label to NFC before encoding: unicodedata.normalize('NFC', label).
  2. Configure upstream storage/transport to preserve NFC (or normalize on ingest).
  3. Run a self-check: assert unicodedata.normalize('NFC', label) == label before idna.

Example fix

# before
idna.encode('á')  # 'a' + combining acute -> not NFC -> Label must be in NFC

# after
import unicodedata
idna.encode(unicodedata.normalize('NFC', 'á'))  # becomes 'á'
Defensive patterns

Strategy: validation

Validate before calling

import unicodedata
def ensure_nfc(label: str) -> str:
    n = unicodedata.normalize('NFC', label)
    if n != label:
        raise ValueError('label is not in NFC')
    return n

Type guard

import unicodedata
def is_nfc(label: str) -> bool:
    return unicodedata.normalize('NFC', label) == label

Try / catch

from idna import IDNAError
try:
    idna.encode(label)
except IDNAError as e:
    if 'Normalization Form C' in str(e):
        import unicodedata
        label = unicodedata.normalize('NFC', label)  # retry normalized
    else:
        raise

Prevention

When it happens

Trigger: A label containing decomposed characters (e.g. 'á' represented as 'a' + '́' instead of the precomposed U+00E1), or any sequence whose NFC differs from the input. Triggered during alabel/check_label/check_nfc.

Common situations: Data entered on systems that decompose accents (macOS HFS+ filenames, some NFD-by-default pipelines); copy-paste from sources using compatibility forms; concatenation of fragments normalized differently.

Related errors


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