pypa/pip · error · UndefinedComparison

Undefined on and .

Error message

Undefined {op!r} on {lhs!r} and {rhs!r}.

What it means

Raised by _eval_op in markers.py:254-256 during Marker.evaluate. _operators supports only 'in','not in','<','<=','==','!=','>=','>'. Version-requiring keys (python_version, python_full_version, implementation_version, platform_release) are routed through a Specifier first, so any operator works for them. For all other keys, an operator not in the table — most notably '~=' (compatible-release) — yields UndefinedComparison (a ValueError).

Solutions

  1. Use '==' / '!=' (or 'in'/'not in') for non-version string markers.
  2. Reserve '~=' for version markers: python_version, python_full_version, implementation_version, platform_release.

Example fix

# before
Marker('os_name ~= "posix"').evaluate()
# after
Marker('os_name == "posix"').evaluate()
Defensive patterns

Strategy: validation

Validate before calling

from packaging.markers import MARKERS_REQUIRING_VERSION

_ALLOWED_STRING_OPS = {"in", "not in", "<", "<=", "==", "!=", ">=", ">"}

def op_allowed_for_key(op_str: str, env_key: str) -> bool:
    return env_key in MARKERS_REQUIRING_VERSION or op_str in _ALLOWED_STRING_OPS

Try / catch

from packaging.markers import Marker, UndefinedComparison

try:
    Marker(expr).evaluate()
except UndefinedComparison as e:
    # unsupported operator for this marker variable
    ...

Prevention

When it happens

Trigger: Evaluating Marker('os_name ~= "posix"'), Marker('sys_platform ~= "linux"'), or any marker using ~= against a non-version environment variable (os_name, sys_platform, platform_machine, platform_python_implementation, implementation_name, extra, etc.).

Common situations: Copying a PEP 440 version-specifier operator into a platform/OS marker; assuming every PEP 440 operator works on string-valued markers.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/80850c1101a5d096. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_vendor/packaging/markers.py:256

    "!=": operator.ne,
    ">=": operator.eq,
    ">": lambda _lhs, _rhs: False,
}


def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
    op_str = op.serialize()
    if key in MARKERS_REQUIRING_VERSION:
        try:
            spec = Specifier(f"{op_str}{rhs}")
        except InvalidSpecifier:
            pass
        else:
            return spec.contains(lhs, prereleases=True)

    oper: Operator | None = _operators.get(op_str)
    if oper is None:
        raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")

    return oper(lhs, rhs)


def _normalize(
    lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[str]]:
    # PEP 685 - Comparison of extra names for optional distribution dependencies
    # https://peps.python.org/pep-0685/
    # > When comparing extra names, tools MUST normalize the names being
    # > compared using the semantics outlined in PEP 503 for names
    if key == "extra":
        assert isinstance(rhs, str), "extra value must be a string"
        # Both sides are normalized at this point already
        return (lhs, rhs)
    if key in MARKERS_ALLOWING_SET:
        if isinstance(rhs, str):  # pragma: no cover
            return (canonicalize_name(lhs), canonicalize_name(rhs))

View on GitHub (pinned to f399c37189)