pypa/pip · error · InvalidCodepoint

Codepoint {_unot(cp_value)} at position {pos + 1} of {label!

Error message

Codepoint {_unot(cp_value)} at position {pos + 1} of {label!r} not allowed

What it means

Raised by check_label (core.py:372) when a codepoint is neither PVALID, CONTEXTJ, nor CONTEXTO — i.e. it is DISALLOWED or UNASSIGNED under IDNA 2008 (RFC 5892). The codepoint is rejected outright regardless of context; the exception class is InvalidCodepoint and the message reports the codepoint as U+XXXX and its position. This is the catch-all for characters the IDNA tables simply do not permit in a label.

Source

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

    check_initial_combiner(label)

    for pos, cp in enumerate(label):
        cp_value = ord(cp)
        if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]):
            continue
        if intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]):
            try:
                if not valid_contextj(label, pos):
                    raise InvalidCodepointContext(f"Joiner {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}")
            except ValueError as err:
                raise IDNAError(
                    f"Unknown codepoint adjacent to joiner {_unot(cp_value)} at position {pos + 1} in {label!r}"
                ) from err
        elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]):
            if not valid_contexto(label, pos):
                raise InvalidCodepointContext(f"Codepoint {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}")
        else:
            raise InvalidCodepoint(f"Codepoint {_unot(cp_value)} at position {pos + 1} of {label!r} not allowed")

    check_bidi(label)


def alabel(label: str) -> bytes:
    """Convert a single U-label into its A-label form.

    The result is the ASCII-Compatible Encoding (ACE) form per :rfc:`5891`
    §4: the label is validated, Punycode-encoded, and prefixed with
    ``xn--``. Pure ASCII labels that are already valid IDNA labels are
    returned unchanged (as :class:`bytes`).

    :param label: The label to convert, as a Unicode string.
    :returns: The A-label as ASCII-encoded :class:`bytes`.
    :raises IDNAError: If the label is invalid or the resulting A-label
        exceeds 63 octets.
    """
    if len(label) > _max_input_length:

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Strip or replace the reported codepoint (the U+XXXX in the message) — most often an underscore, wildcard, emoji, or invisible format character — before encoding.
  2. Validate domains against a DNS-safe charset (RFC 1035 LDH: letters, digits, hyphen) before passing them to idna; reject or normalise anything else upstream.
  3. If you genuinely need permissive Unicode handling, call encode(domain, uts46=True, std3_rules=False) so UTS #46 mapping tolerates more input, or upgrade the idna/Python version to refresh the Unicode tables for recently-assigned codepoints.

Example fix

// before
idna.encode('_acme-challenge.example.com')  # underscore is DISALLOWED

// after
# validate/strip non-LDH characters first, or handle the SRV-style label separately
host = '_acme-challenge.example.com'
if host.startswith('_'):
    # SRV / ACME records are not idna-encodable; pass through as-is
    encoded = host.encode('ascii')
else:
    encoded = idna.encode(host)
Defensive patterns

Strategy: validation

Validate before calling

import idna, re

LDH_RE = re.compile(r'^[A-Za-z0-9-]+$')

def is_dns_safe_label(label: str) -> bool:
    # quick LDH (Letters-Digits-Hyphen) gate; idna.check_label is the full check
    if not label or len(label) > 63:
        return False
    if label.startswith('-') or label.endswith('-'):
        return False
    if not LDH_RE.match(label):
        # non-ASCII allowed only via idna; defer to check_label
        try:
            idna.check_label(label)
        except idna.IDNAError:
            return False
    return True

Type guard

def is_ascii_ldh(s) -> bool:
    return isinstance(s, str) and bool(s) and all(
        'a' <= c <= 'z' or 'A' <= c <= 'Z' or '0' <= c <= '9' or c == '-' for c in s
    )

Try / catch

import idna

try:
    encoded = idna.encode(domain)
except idna.InvalidCodepoint as err:
    # disallowed/unassigned codepoint; reject the input
    raise ValueError(f'invalid domain {domain!r}: {err}') from err

Prevention

When it happens

Trigger: Calling idna.encode(), idna.alabel(), or idna.check_label() on a label containing a disallowed or unassigned character: control characters, symbols such as underscore in the wrong position, emoji, currency signs, punctuation outside the allowed set, or a codepoint allocated in a newer Unicode version than the vendored idnadata tables know about.

Common situations: Config/host files containing underscores or wildcards ('_acme-challenge', '*.'), copy-pasted domains containing invisible formatting characters (BOM, zero-width spaces), emoji subdomains, emoji in email/HTTP Host headers reaching an idna encode path, or running an older idna release against input using recently-assigned Unicode scripts.

Related errors


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