pypa/pip · error · TypeError

VersionRange.contains() expected str or Version, got {type(i

Error message

VersionRange.contains() expected str or Version, got {type(item).__name__}

What it means

TypeError raised by VersionRange.contains when the 'item' argument is neither a str nor a packaging.version.Version. Membership testing requires something parseable as a version (or already a Version object); other types are rejected upfront rather than coerced.

Source

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

        opt-in region; it reads only the configured policy. This mirrors
        :meth:`~packaging.specifiers.SpecifierSet.contains` versus
        :meth:`~packaging.specifiers.SpecifierSet.filter`.

        Unparsable strings do not match, except where the full
        ``SpecifierSet`` would also match: the full range admits any string,
        and a ``===`` range admits items equal to the literal
        case-insensitively.

        >>> r = SpecifierSet(">=1.0,<2.0").to_range()
        >>> r.contains("1.5")
        True
        >>> r.contains("2.0")
        False

        :raises TypeError: if item is not a str or Version.
        """
        if not isinstance(item, (str, Version)):
            raise TypeError(
                f"VersionRange.contains() expected str or Version, "
                f"got {type(item).__name__}"
            )

        parsed: Version | None = item if isinstance(item, Version) else None
        if installed and parsed is None:
            parsed = coerce_version(item)
        if installed and parsed is not None and parsed.is_prerelease:
            prereleases = True

        effective_pre = (
            self._prereleases_configured if prereleases is None else prereleases
        )

        if self._admit or self._reject:
            item_str = str(item).lower()
            if item_str in self._reject:
                return False

View on GitHub (pinned to f399c37189)

Solutions

  1. Convert the value to a string before testing: range.contains(str(value)).
  2. For numeric inputs, format deterministically (e.g. str(1.5) -> '1.5') or build a Version explicitly: Version('1.5').
  3. Validate/normalize incoming version data at your API boundary so only str or Version reaches contains().
  4. If you often have a Version already, pass it directly — that path avoids re-parsing.

Example fix

# before
r = SpecifierSet('>=1.0').to_range()
r.contains(1.5)  # TypeError

# after
r = SpecifierSet('>=1.0').to_range()
r.contains('1.5')
# or
from packaging.version import Version
r.contains(Version('1.5'))
Defensive patterns

Strategy: type-guard

Validate before calling

null

Type guard

from packaging.version import Version

def coerce_for_contains(item):
    if isinstance(item, (str, Version)):
        return item
    if isinstance(item, (int, float)):
        return str(item)
    raise TypeError(f"version membership needs str/Version, got {type(item).__name__}")

# then: r.contains(coerce_for_contains(value))

Try / catch

try:
    ok = r.contains(value)
except TypeError:
    # value was not str/Version; coerce via coerce_for_contains() and retry
    raise

Prevention

When it happens

Trigger: Calling range.contains(123) (int), range.contains(1.5) (float), range.contains(None), or range.contains(some_other_object). Note: the 'in' operator (__contains__) forwards to contains with default args, so '1.5 in range_float_bug' where you passed a float also hits this.

Common situations: Passing a numeric version (int/float) instead of its string form; feeding a parsed Version from a different library; an unguarded user input that arrived as a non-string.

Related errors


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