pypa/pip · error · IDNAError
A-label must not end with a hyphen
Error message
A-label must not end with a hyphen
What it means
Raised by ulabel() (core.py:441) when the bytes after the 'xn--' prefix end with a hyphen ('-'). Punycode labels are defined so the literal hyphens separate the basic ASCII codepoints from the extensions; a trailing hyphen is not valid Punycode syntax, so the A-label is rejected as malformed before decode is even attempted.
Source
Thrown at src/pip/_vendor/idna/core.py:441
"""
if len(label) > _max_input_length:
raise IDNAError("Label too long")
if not isinstance(label, (bytes, bytearray)):
try:
label_bytes = label.encode("ascii")
except UnicodeEncodeError:
check_label(label)
return label
else:
label_bytes = bytes(label)
label_bytes = label_bytes.lower()
if label_bytes.startswith(_alabel_prefix):
label_bytes = label_bytes[len(_alabel_prefix) :]
if not label_bytes:
raise IDNAError("Malformed A-label, no Punycode eligible content found")
if label_bytes.endswith(b"-"):
raise IDNAError("A-label must not end with a hyphen")
else:
check_label(label_bytes)
return label_bytes.decode("ascii")
try:
label = label_bytes.decode("punycode")
except UnicodeError as err:
raise IDNAError("Invalid A-label") from err
check_label(label)
return label
def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str:
"""Apply the UTS #46 character mapping to a domain string.
Implements the mapping table from `UTS #46 §4
<https://www.unicode.org/reports/tr46/>`_: each character is kept,
replaced, or rejected based on its status (``V``, ``M``, ``D``, ``3``,View on GitHub (pinned to d7d0d0a394)
Solutions
- Strip a stray trailing hyphen from the Punycode portion if it is clearly a typo, or regenerate the A-label with idna.alabel() from the source Unicode.
- Validate the A-label shape (matches ^xn--[A-Za-z0-9-]*[A-Za-z0-9]$) before calling ulabel/decode.
- Use decode(domain, display=True) to pass malformed ACE labels through unchanged for display-only consumers.
Example fix
// before
idna.ulabel('xn--abc-') # trailing hyphen in Punycode portion
// after
idna.ulabel('xn--abc') # no trailing hyphen
# or rebuild correctly:
idna.alabel('münchen') # -> b'xn--mnchen-3ya' Defensive patterns
Strategy: validation
Validate before calling
def is_well_formed_alabel(label) -> bool:
s = label.decode('ascii') if isinstance(label, (bytes, bytearray)) else label
lower = s.lower()
if lower.startswith('xn--'):
payload = lower[4:]
if not payload or payload.endswith('-'):
return False
return True Type guard
import re
ACE_RE = re.compile(r'^xn--[A-Za-z0-9-]*[A-Za-z0-9]$', re.IGNORECASE)
def is_valid_ace_label(s) -> bool:
return isinstance(s, str) and bool(ACE_RE.match(s)) Try / catch
import idna
try:
decoded = idna.ulabel(label)
except idna.IDNAError as err:
if 'must not end with a hyphen' in str(err):
decoded = label.rstrip('-') # or regenerate via alabel
else:
raise Prevention
- Validate ACE labels with a regex forbidding a trailing hyphen in the payload.
- Regenerate A-labels from their Unicode source with idna.alabel() rather than editing them.
- Use decode(domain, display=True) for display-only consumers to pass bad labels through.
When it happens
Trigger: Calling idna.ulabel() or idna.decode() with an A-label like 'xn--abc-' where the Punycode portion ends in '-'. Also produced by string manipulation that accidentally appends a hyphen, or by an off-by-one slice of a valid A-label.
Common situations: Templated domains that append a trailing '-' for separator reasons, truncation/slicing bugs, copy-paste of a corrupted IDN, or DNS zone files with a typo'd ACE label.
Related errors
- Malformed A-label, no Punycode eligible content found
- Invalid A-label
- Label has disallowed hyphens in 3rd and 4th position
- Codepoint {_unot(cp_value)} not allowed at position {pos + 1
- Codepoint {_unot(cp_value)} at position {pos + 1} of {label!
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/4530850e28771aaa.json.
Report an issue: GitHub.