pypa/pip · error · IDNABidiError

Invalid direction for codepoint at position {idx} in a left-

Error message

Invalid direction for codepoint at position {idx} in a left-to-right label

What it means

IDNABidiError from check_bidi Bidi Rule 5: in a left-to-right label every codepoint's bidi category must be in the allowed LTR set {L, EN, ES, CS, ET, ON, BN, NSM}. A codepoint outside that set (typically an R/AL/AN RTL character) appearing in a label that started with L makes the label's directionality ambiguous and is rejected at the given position.

Source

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

        if rtl:
            # Bidi rule 2
            if direction not in _bidi_rtl_allowed:
                raise IDNABidiError(f"Invalid direction for codepoint at position {idx} in a right-to-left label")
            # Bidi rule 3
            if direction in _bidi_rtl_valid_ending:
                valid_ending = True
            elif direction != "NSM":
                valid_ending = False
            # Bidi rule 4
            if direction in _bidi_rtl_numeric:
                if not number_type:
                    number_type = direction
                elif number_type != direction:
                    raise IDNABidiError("Can not mix numeral types in a right-to-left label")
        else:
            # Bidi rule 5
            if direction not in _bidi_ltr_allowed:
                raise IDNABidiError(f"Invalid direction for codepoint at position {idx} in a left-to-right label")
            # Bidi rule 6
            if direction in _bidi_ltr_valid_ending:
                valid_ending = True
            elif direction != "NSM":
                valid_ending = False

    if not valid_ending:
        raise IDNABidiError("Label ends with illegal codepoint directionality")

    return True


def check_initial_combiner(label: str) -> bool:
    """Reject labels that begin with a combining mark.

    Per :rfc:`5891` §4.2.3.2 a label must not start with a character of
    Unicode general category ``M`` (Mark).

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Keep LTR labels free of R/AL/AN codepoints; move RTL text into its own label.
  2. Pre-filter input with unicodedata.bidirectional and reject any RTL char in an L-started label.
  3. Use idna.encode with uts46=True and std3_rules=True to surface the problem earlier as an InvalidCodepoint.

Example fix

# before
label = 'testا'
idna.check_bidi(label, check_ltr=True)  # Invalid direction in LTR label

# after
label = 'test'
idna.check_bidi(label, check_ltr=True)
Defensive patterns

Strategy: validation

Validate before calling

import unicodedata
_LTR_ALLOWED = {'L','EN','ES','CS','ET','ON','BN','NSM'}
def is_pure_ltr_label(label: str) -> bool:
    return bool(label) and unicodedata.bidirectional(label[0]) == 'L' and all(unicodedata.bidirectional(c) in _LTR_ALLOWED for c in label)

Type guard

import unicodedata
def is_ltr_label_clean(label: str) -> bool:
    return all(unicodedata.bidirectional(c) in {'L','EN','ES','CS','ET','ON','BN','NSM'} for c in label)

Try / catch

from idna import IDNABidiError
try:
    idna.check_bidi(label, check_ltr=True)
except IDNABidiError as e:
    if 'left-to-right' in str(e):
        # remove RTL codepoints or relocate them to a separate label
        raise ValueError('RTL codepoint in LTR label') from e
    raise

Prevention

When it happens

Trigger: An L-started label into which an RTL character (Hebrew, Arabic, AN digit) is inserted, e.g. 'testا'. Triggered when check_ltr=True or when the LTR label is being explicitly bidi-checked; also when the label is being checked because it was passed to check_bidi with check_ltr.

Common situations: Usernames or slugs that concatenate ASCII prefixes with localized RTL suffixes; automated domain generators that merge keywords from mixed locales; sanitizer that allows non-ASCII but does not enforce script purity.

Related errors


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