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
- Normalize the label to NFC before encoding: unicodedata.normalize('NFC', label).
- Configure upstream storage/transport to preserve NFC (or normalize on ingest).
- 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
- Normalize all input to NFC at the system ingress.
- Run a self-check: assert unicodedata.normalize('NFC', s) == s before idna.
- Be wary of NFD-by-default filesystems (legacy macOS HFS+) when reading labels.
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
- Label begins with an illegal combining character
- Unknown directionality in label {label!r} at position {idx}
- First codepoint in label {label!r} must be directionality L,
- Label has disallowed hyphens in 3rd and 4th position
- Label must not start or end with a hyphen
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/1b8457738a4a9c1d.json.
Report an issue: GitHub.