pypa/pip · error · ValueError

Cannot combine VersionRange operands with different pre-rele

Error message

Cannot combine VersionRange operands with different pre-release policies: {self._prereleases_configured!r} and {other._prereleases_configured!r}

What it means

ValueError raised by VersionRange._check_policy_compat when both operands are VersionRange but their explicit pre-release policies (_prereleases_configured, i.e. an explicit True/False prereleases override on the originating SpecifierSet) differ. Combining them would silently change which pre-releases are admitted, so the library refuses and reports both policy values.

Source

Thrown at src/pip/_vendor/packaging/ranges.py:1089

        """
        return self._admit_arbitrary and self._bounds == FULL_RANGE

    def _is_plain(self) -> bool:
        """True when membership is decided by ``_bounds`` alone, enabling the
        bounds-only fast paths in :meth:`is_subset` and :meth:`is_disjoint`.
        """
        return (
            not self._has_literals()
            and not self._admit_arbitrary
            and self._prereleases_configured is not False
        )

    def _check_policy_compat(self, other: VersionRange) -> None:
        """Refuse combining ranges with different pre-release policies."""
        if not isinstance(other, VersionRange):
            raise TypeError(f"expected VersionRange, got {type(other).__name__}")
        if self._prereleases_configured != other._prereleases_configured:
            raise ValueError(
                "Cannot combine VersionRange operands with different "
                f"pre-release policies: {self._prereleases_configured!r} "
                f"and {other._prereleases_configured!r}"
            )

    def _merged_region(self, other: VersionRange) -> tuple[Interval, ...]:
        """Union of ``self`` and ``other``'s opt-in regions.

        Used by :meth:`union` and :meth:`intersection`; :meth:`_build` clips the
        merge to the result bounds. A configured operand carries an empty region,
        so it contributes nothing to the merge.
        """
        # Reuse an operand's canonical tuple when only one side has a region;
        # an empty side contributes nothing to the union.
        if not other._pre_region:
            return self._pre_region
        if not self._pre_region:
            return other._pre_region

View on GitHub (pinned to f399c37189)

Solutions

  1. Inspect each operand's _prereleases_configured (the values are shown in the message) and reconcile them.
  2. Rebuild the originating SpecifierSets with the same explicit prereleases value (or None on both) before converting to ranges.
  3. If one operand should govern the policy, drop the other's override (use None) and set it to match.
  4. Where policy genuinely must differ, evaluate membership separately rather than combining the ranges.

Example fix

# before
a = SpecifierSet('>=1.0', prereleases=True).to_range()
b = SpecifierSet('<2.0', prereleases=False).to_range()
combined = a.union(b)  # ValueError

# after
a = SpecifierSet('>=1.0', prereleases=True).to_range()
b = SpecifierSet('<2.0', prereleases=True).to_range()
combined = a.union(b)
Defensive patterns

Strategy: validation

Validate before calling

from packaging.ranges import VersionRange
from packaging.specifiers import SpecifierSet

def same_prerelease_policy(a: VersionRange, b: VersionRange) -> bool:
    return a._prereleases_configured == b._prereleases_configured

def safe_combine(a, b, op):
    if not same_prerelease_policy(a, b):
        # reconcile: rebuild b from its spec without an override, or match a's policy
        b = SpecifierSet(str(b)).to_range()  # default policy
    return op(a, b)

Type guard

null

Try / catch

try:
    combined = a.union(b)
except ValueError as e:
    # differing prereleases policies; rebuild operands with matching/None policy
    raise

Prevention

When it happens

Trigger: Calling union/intersection/difference/etc. between ranges whose source SpecifierSets had different prereleases overrides, e.g. SpecifierSet('>=1.0', prereleases=True).to_range().union(SpecifierSet('<2.0', prereleases=False).to_range()). The values True/False/None must match.

Common situations: Mixing ranges derived from specifier sets configured with explicit prereleases=True/False in different parts of a resolver; a helper that builds ranges with a hardcoded override combined with user-supplied ranges that have None.

Related errors


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