pypa/pip · error · InvalidCodepoint

Codepoint {_unot(code_point)} not allowed at position {pos +

Error message

Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}

What it means

Raised by uts46_remap() (core.py:505) when a codepoint's UTS #46 status is not V (valid), D (deviation), 3 (disallowed-STD3), or I (ignored) — i.e. it is outright disallowed under UTS #46 and std3/transition handling does not rescue it. The exception class is InvalidCodepoint and the message names the codepoint (U+XXXX) and its position. This is the UTS #46 counterpart of the IDNA 2008 disallowed-codepoint error (261), reached when uts46=True is used.

Source

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

        # 3 is disallowed-STD3 (kept unmapped if std3_rules is off and no mapping).
        keep_as_is = (
            status == "V" or (status == "D" and not transitional) or (status == "3" and not std3_rules and replacement is None)
        )
        # M is mapped, 3-with-replacement and transitional D fall through to the
        # same replacement output path.
        use_replacement = replacement is not None and (
            status == "M" or (status == "3" and not std3_rules) or (status == "D" and transitional)
        )

        if keep_as_is:
            output += char
        elif use_replacement:
            assert replacement is not None  # narrowed by use_replacement
            output += replacement
        elif status == "I":
            continue
        else:
            raise InvalidCodepoint(f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}")

    return unicodedata.normalize("NFC", output)


def encode(
    s: Union[str, bytes, bytearray],
    strict: bool = False,
    uts46: bool = False,
    std3_rules: bool = False,
    transitional: bool = False,
) -> bytes:
    """Encode a Unicode domain name into its ASCII (A-label) form.

    Splits the input on label separators (only ``U+002E`` if ``strict`` is
    set; otherwise also IDEOGRAPHIC FULL STOP ``U+3002``, FULLWIDTH FULL
    STOP ``U+FF0E``, and HALFWIDTH IDEOGRAPHIC FULL STOP ``U+FF61``),
    encodes each label with :func:`alabel`, and rejoins them with ``.``.
    Optionally pre-processes the input through :func:`uts46_remap`.

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Remove or replace the reported codepoint (U+XXXX) with an LDH-safe character before encoding.
  2. Call uts46_remap(domain, std3_rules=False) to relax the STD3 rules so more ASCII punctuation is tolerated, if your use case allows it.
  3. Sanitise the domain at the input boundary: NFC-normalise, strip control/format categories, and restrict to an allow-list before handing to idna.

Example fix

// before
idna.encode('a_b.example', uts46=True, std3_rules=True)  # '_' disallowed

// after
idna.encode('a_b.example', uts46=True, std3_rules=False)  # tolerate '_'
# or strip non-LDH first:
import unicodedata
clean = ''.join(c for c in domain if unicodedata.category(c)[0] not in ('C','Z'))
idna.encode(clean, uts46=True)
Defensive patterns

Strategy: validation

Validate before calling

import idna

def uts46_safe(domain: str, std3_rules: bool = False) -> str:
    try:
        return idna.uts46_remap(domain, std3_rules=std3_rules)
    except idna.InvalidCodepoint:
        raise ValueError(f'domain {domain!r} contains a UTS #46-disallowed codepoint')

remapped = uts46_safe(domain, std3_rules=False)

Type guard

import unicodedata

def is_uts46_plausible(s) -> bool:
    if not isinstance(s, str):
        return False
    return all(unicodedata.category(c)[0] not in ('C', 'Z', 'M') for c in s)

Try / catch

import idna

try:
    encoded = idna.encode(domain, uts46=True)
except idna.InvalidCodepoint as err:
    raise ValueError(f'invalid domain {domain!r}: {err}') from err

Prevention

When it happens

Trigger: Calling uts46_remap() directly, or encode()/decode() with uts46=True, on a domain containing a codepoint UTS #46 marks disallowed — control characters, most symbols/punctuation, emoji, or private-use codepoints. With std3_rules=True (default for uts46_remap) additional ASCII punctuation like '_' also falls here when no mapping is defined.

Common situations: Emoji or symbol hostnames, underscores in uts46=True mode, copy-pasted domains with invisible control/format characters, private-use or unassigned codepoints, or input from systems that did no Unicode normalisation.

Related errors


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