Version Ranges

A VersionRange is the set of Version values accepted by a SpecifierSet, viewed as intervals on the PEP 440 ordering. It supports intersection, union, complement, and difference, so tooling that combines many requirements, such as a resolver, can work on the intervals directly.

Usage

>>> from packaging.ranges import VersionRange
>>> from packaging.specifiers import SpecifierSet
>>> from packaging.version import Version
>>> # Build a range from a specifier set
>>> r = SpecifierSet(">=1.0,<2.0").to_range()
>>> r
<VersionRange '[1.0, 2.0.dev0)'>
>>> Version("1.5") in r
True
>>> Version("2.0") in r
False
>>> # Combine ranges with set algebra
>>> a = SpecifierSet(">=1.0").to_range()
>>> b = SpecifierSet("<2.0").to_range()
>>> a & b == r
True
>>> # The union covers either side, leaving any gap between them
>>> u = SpecifierSet("<1.0").to_range() | SpecifierSet(">=2.0").to_range()
>>> Version("0.5") in u
True
>>> Version("1.5") in u
False
>>> # The complement is every other version
>>> Version("0.5") in ~r
True
>>> # Filter an iterable of versions
>>> list(r.filter(["0.9", "1.5", "2.0"]))
['1.5']
>>> # An unsatisfiable set produces the empty range
>>> SpecifierSet(">=2.0,<1.0").to_range().is_empty
True

Pre-releases

A specifier that names a pre-release, such as >=2.0b1, opts in pre-releases only for the versions it asks for. That opt-in region is carried as ranges are combined, so a union with a plain range keeps the pre-releases it named without admitting every pre-release below them:

>>> a = SpecifierSet(">=1.0").to_range()
>>> b = SpecifierSet(">=2.0b1").to_range()
>>> list((a | b).filter(["1.5b1", "2.0b1", "2.5"]))
['2.0b1', '2.5']

2.0b1 is admitted because >=2.0b1 asked for it; 1.5b1 is not, since the opt-in never came from >=1.0.

The opt-in is also clipped to the range’s own bounds, so combining ranges never force-admits a pre-release outside every operand’s request. >=2.0b1,<3 opts pre-releases in only below 3, so unioning it with an unrelated higher range does not admit a pre-release up in that range:

>>> capped = SpecifierSet(">=2.0b1,<3").to_range()
>>> other = SpecifierSet(">=3.5,<4").to_range()
>>> list((capped | other).filter(["3.6b1", "3.7"]))
['3.7']

Neither operand admits 3.6b1: >=3.5,<4 names no pre-release and >=2.0b1,<3 opts in only below 3.

Set difference

a - b is set difference: the versions in a but not b. It agrees with a & ~b on the version set and the opt-in region, so subtracting a pre-release-naming range does not leak its pre-releases into the result:

>>> base = SpecifierSet(">=1.0").to_range()
>>> excluded = SpecifierSet(">=2.0b1").to_range()
>>> list((base - excluded).filter(["1.9", "2.0a1"]))
['1.9']
>>> (base - excluded) == (base & ~excluded)
True

Comparing ranges

Equality on a VersionRange is structural: it compares the bounds, the === admit/reject literals, the arbitrary-string flag, the configured pre-release policy, and the opt-in region, not only the version set. Equal ranges therefore behave identically under VersionRange.contains() and VersionRange.filter(). The guarantee is one-directional: ==1.0 and >=1.0,<1.0.post0.dev0 compare unequal (the second carries an opt-in region), yet no pre-release exists in that window, so they filter identically.

For set relations use VersionRange.is_subset(), VersionRange.is_superset(), and VersionRange.is_disjoint() rather than comparing intersections by hand. Intersection can change the opt-in region without changing the version set, so the textbook subset test a & b == a can report a false negative. Each method compares the version sets directly, so it is not affected by that opt-in difference:

>>> from packaging.specifiers import SpecifierSet
>>> a = SpecifierSet(">=1.0").to_range()
>>> b = SpecifierSet(">=1.0a1").to_range()
>>> # Every version >=1.0 is also >=1.0a1, so a is a subset of b. But b
>>> # opts pre-releases in, so ``a & b`` and ``a`` differ in the opt-in region:
>>> a & b == a
False
>>> a.is_subset(b)
True
>>> b.is_superset(a)
True
>>> a.is_disjoint(b)
False
>>> # The opt-in difference is observable: a & b force-admits pre-releases
>>> # in b's region that plain a filters out
>>> list(a.filter(["2.0a1", "2.5"]))
['2.5']
>>> list((a & b).filter(["2.0a1", "2.5"]))
['2.0a1', '2.5']

Like VersionRange.intersection(), VersionRange.union(), and VersionRange.difference(), these predicates require both operands to share the same configured pre-release policy and raise ValueError otherwise.

Different specifiers that denote the same range, opt-in region included, canonicalize to one form, so they compare equal. >1.0a1 excludes 1.0a1’s post-releases per PEP 440, so its smallest member is 1.0a2.dev0, exactly the set of >=1.0a2.dev0:

>>> r1 = SpecifierSet(">1.0a1").to_range()
>>> r2 = SpecifierSet(">=1.0a2.dev0").to_range()
>>> r1 == r2
True
>>> third = SpecifierSet("<2.0").to_range()
>>> (r1 & third) == (r2 & third)
True

The opt-in region is also part of equality. <1.0.post0.dev0 and <=1.0 cover the same versions, but the first autodetects an opt-in region from its .dev bound, so it admits pre-releases by default, while the second does not; they are not substitutable and compare unequal:

>>> SpecifierSet("<1.0.post0.dev0").to_range() == SpecifierSet("<=1.0").to_range()
False

Limits of the model

The pre-release opt-in is not itself a set. PEP 440 opts a whole specifier set in when any one specifier names a pre-release, so & is requirement conjunction (a comma-merge of the two specifier sets), not plain intersection, on the opt-in. No model can keep that parity with SpecifierSet, exclusion soundness (an exclusion grants no opt-in), and an involutive complement at the same time. This model keeps parity and soundness, so complement drops the opt-in and a few Boolean identities do not hold once an operand carries one.

A double complement therefore erases the opt-in rather than restoring it. ~~r keeps r’s versions but not its eager pre-releases:

>>> b = SpecifierSet(">=2.0b1").to_range()
>>> list(b.filter(["2.0b1", "2.5"]))
['2.0b1', '2.5']
>>> list((~~b).filter(["2.0b1", "2.5"]))
['2.5']
>>> ~~b == b
False

The erasure is still well behaved: ~~~r == ~r, both De Morgan laws hold, and r & ~r is empty.

>>> ~~~b == ~b
True
>>> (b & ~b).is_empty
True

Because complement drops the opt-in while union keeps it, b | full() carries b’s opt-in, which the full range (with no eager pre-release) does not, so it does not collapse to VersionRange.full(). For the same reason union no longer distributes over intersection, and the absorption laws do not hold, when an operand carries an opt-in:

>>> a = SpecifierSet(">=1.0").to_range()
>>> b | VersionRange.full() == VersionRange.full()
False
>>> a & (a | b) == a
False

Intersection still distributes over union, a - b agrees with a & ~b on the version set and the opt-in region, and every law holds on the version set as usual. The opt-in region makes the identities above differ only when a specifier named a pre-release.

The arbitrary-string flag has corners of its own. SpecifierSet("") matches even strings that are not PEP 440 versions, so its range, VersionRange.full(), admits them too; pass admit_arbitrary=False for the versions-only full range. Only those ranges and === literals admit such strings: combining ranges never grants an admission the operands did not have, so ~r stays version-only for any version-only r. The flag rides along where that is harmless, keeping ~~full() == full() and union idempotent, but an intersection or difference that shrinks the bounds does not remember it:

>>> f = VersionRange.full()
>>> "wat" in f
True
>>> "wat" in VersionRange.full(admit_arbitrary=False)
False
>>> ~~f == f
True
>>> (~f | ~f) == ~f
True
>>> (f & ~f) == VersionRange.empty()
True
>>> (f & ~f) == ~f
False
>>> d = f - SpecifierSet(">=1.0").to_range()
>>> "wat" in (d | SpecifierSet(">=0.5").to_range())
False
>>> d == f & ~SpecifierSet(">=1.0").to_range()
True

Reference

Public VersionRange API.

A set-algebra view of the versions accepted by a SpecifierSet. Ranges support intersection, union, complement, and difference; membership and filtering match the originating specifier set.

class packaging.ranges.VersionRange

A set of Version values accepted by a SpecifierSet.

Construct via to_range(), or with the full(), empty(), and singleton() class methods. Compose with intersection(), union(), complement(), and difference() (or the & / | / ~ / - operators). Test membership with in or contains(), filter an iterable with filter().

The configured pre-release policy of the originating specifier set carries onto the range and controls whether pre-releases are admitted under in, contains(), and filter(). With no configured policy, filter() also admits pre-releases in the autodetected opt-in region (the versions a pre-release-naming specifier asked for). Set algebra keeps that opt-in scoped to those versions, so unrelated pre-releases are not admitted wholesale.

intersection(), union(), difference(), and the is_subset() / is_superset() / is_disjoint() predicates require both operands to share the same configured policy.

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

PEP 440’s === operator matches a candidate string verbatim (case-insensitive) rather than a set of versions. Ranges built from === specifiers still support membership and set operations; matching follows the literal-equality rule. A === literal that names a pre-release is admitted under the default policy by both contains() and filter(), since it was named outright.

Added in version 26.3.

static __new__(cls, *args, **kwargs)
Parameters:
Return type:

VersionRange

classmethod empty(*, prereleases=None)

Return the empty range. No version satisfies it.

>>> VersionRange.empty().is_empty
True
>>> "1.0" in VersionRange.empty()
False
Parameters:

prereleases (bool | None)

Return type:

VersionRange

classmethod full(*, admit_arbitrary=True, prereleases=None)

Return the full range. Every PEP 440 version satisfies it.

admit_arbitrary=False restricts the range to PEP 440 versions only (matching the same versions as SpecifierSet(">=0.dev0").to_range()); its complement is empty(). The flag propagates through set algebra and is part of equality. Default True so that r & full() preserves r’s own flag structurally.

>>> "1.0" in VersionRange.full()
True
>>> "wat" in VersionRange.full()
True
>>> "wat" in VersionRange.full(admit_arbitrary=False)
False
Parameters:
  • admit_arbitrary (bool)

  • prereleases (bool | None)

Return type:

VersionRange

classmethod singleton(version, *, prereleases=None)

Return the strict singleton range {version}.

Built as the closed interval [version, version] with strict equality. Specifier("==V") matches V+local too, so the strict singleton is narrower:

>>> "1.0+local" in VersionRange.singleton("1.0")
False
>>> "1.0+local" in SpecifierSet("==1.0").to_range()
True
Raises:

packaging.version.InvalidVersion – if version is a string that does not parse as a PEP 440 version.

Parameters:
Return type:

VersionRange

intersection(other)

Range containing exactly the versions in both self and other.

Both operands must share the same configured pre-release policy; otherwise ValueError is raised.

>>> a = SpecifierSet(">=1.0").to_range()
>>> b = SpecifierSet("<2.0").to_range()
>>> a.intersection(b) == SpecifierSet(">=1.0,<2.0").to_range()
True
Parameters:

other (VersionRange)

Return type:

VersionRange

union(other)

Range containing every version in self or other.

Both operands must share the same configured pre-release policy; otherwise ValueError is raised.

>>> a = VersionRange.singleton("1.0")
>>> b = VersionRange.singleton("2.0")
>>> "1.0" in a.union(b) and "2.0" in a.union(b)
True
>>> "1.5" in a.union(b)
False
Parameters:

other (VersionRange)

Return type:

VersionRange

complement()

Range containing every version not in self.

Preserves the configured pre-release policy. On the version set, double negation holds for a range with no === literals (the arbitrary-string flag round-trips, so ~~full() == full()); for === ranges complement is one-way. The opt-in region is not restored (see below).

The opt-in region is dropped: a complement is an exclusion, and an exclusion expresses no pre-release preference. This is what lets a & ~b shed b’s opt-in, so an excluded b never force-admits a pre-release into the result. Complement stays involutive on the version set, but not on the opt-in region: ~~r covers the same versions as r yet force-admits none of its pre-releases.

>>> r = SpecifierSet(">=1.0").to_range()
>>> "0.5" in r.complement()
True
>>> "1.5" in r.complement()
False
>>> r.complement().complement() == r
True
Return type:

VersionRange

difference(other)

Range containing the versions in self but not in other.

Matches self & ~other on the version set and the opt-in region; other acts as a bounds-only exclusion that grants no opt-in. The arbitrary-string flag survives only when other removed no versions: a difference that shrinks the bounds forgets it, as self & ~other would, so no later widening union can revive it. They still part on === literals, whose complement is one-way: a === literal stays when self admits it and other does not. Both operands must share the same configured pre-release policy (as intersection() and union() require); otherwise ValueError is raised. a - empty() returns a range equal to a.

>>> a = SpecifierSet(">=1.0").to_range()
>>> b = SpecifierSet(">=2.0").to_range()
>>> "1.5" in a.difference(b)
True
>>> "2.0" in a.difference(b)
False
>>> a.difference(VersionRange.empty()) == a
True
Parameters:

other (VersionRange)

Return type:

VersionRange

__and__(other)

Operator alias for intersection().

Parameters:

other (object)

Return type:

VersionRange

__or__(other)

Operator alias for union().

Parameters:

other (object)

Return type:

VersionRange

__invert__()

Operator alias for complement().

Return type:

VersionRange

__sub__(other)

Operator alias for difference().

Parameters:

other (object)

Return type:

VersionRange

is_subset(other)

Return whether every member of self is also a member of other.

On versions and === literals this is self.difference(other).is_empty: subtracting other leaves nothing behind. A live arbitrary admission (the flag at full bounds) is only a subset of another live one.

Both operands must share the same configured pre-release policy; otherwise ValueError is raised.

>>> inner = SpecifierSet(">=1.5,<1.8").to_range()
>>> outer = SpecifierSet(">=1.0,<2.0").to_range()
>>> inner.is_subset(outer)
True
>>> outer.is_subset(inner)
False
>>> VersionRange.empty().is_subset(outer)
True
Parameters:

other (VersionRange)

Return type:

bool

is_superset(other)

Return whether every member of other is also a member of self.

The mirror of is_subset(): a.is_superset(b) is b.is_subset(a).

Both operands must share the same configured pre-release policy; otherwise ValueError is raised.

>>> outer = SpecifierSet(">=1.0,<2.0").to_range()
>>> outer.is_superset(SpecifierSet(">=1.5,<1.8").to_range())
True
Parameters:

other (VersionRange)

Return type:

bool

is_disjoint(other)

Return whether self and other share no member.

Equivalent to (self & other).is_empty.

Both operands must share the same configured pre-release policy; otherwise ValueError is raised.

>>> a = SpecifierSet(">=1.0,<2.0").to_range()
>>> a.is_disjoint(SpecifierSet(">=2.0,<3.0").to_range())
True
>>> a.is_disjoint(SpecifierSet(">=1.5,<2.5").to_range())
False
Parameters:

other (VersionRange)

Return type:

bool

filter(iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None, key: None = None) Iterator[UnparsedVersionVar]
filter(iterable: Iterable[T], prereleases: bool | None = None, key: Callable[[T], UnparsedVersion] = None) Iterator[T]

Yield items from iterable whose version falls inside the range.

With prereleases None the PEP 440 default applies: pre-releases are buffered and only emitted if no final release in iterable is in range, except that a pre-release inside the autodetected opt-in region, or named outright by a === literal, is force-admitted in place (as prereleases=True would yield it). A flushed buffer comes after every in-place yield, so the output is not version-sorted.

The signature mirrors filter().

>>> r = SpecifierSet(">=1.0,<2.0").to_range()
>>> list(r.filter(["0.9", "1.5", "2.0"]))
['1.5']
Parameters:
  • iterable (Iterable[Any])

  • prereleases (bool | None)

  • key (Callable[[Any], Version | str] | None)

Return type:

Iterator[Any]

property is_empty: bool

True if no version or string satisfies this range.

Agrees with is_unsatisfiable(), including the pre-release policy: a range whose only members are pre-releases is empty when that policy excludes them.

>>> SpecifierSet(">=2,<1").to_range().is_empty
True
>>> SpecifierSet(">=1,<2").to_range().is_empty
False
>>> SpecifierSet("==1.0a1", prereleases=False).to_range().is_empty
True
contains(item, prereleases=None, installed=None)

Return whether item is contained in this range.

Parameters:
  • item (Version | str) – a version string or Version.

  • prereleases (bool | None) – whether to match pre-releases. None (default) uses the range’s own policy.

  • installed (bool | None) – when True, accept a pre-release item even if the range would not otherwise allow it.

Return type:

bool

Unlike filter(), this does not consult the autodetected pre-release opt-in region; it reads only the configured policy. This mirrors contains() versus 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.

Parameters:
Return type:

bool

__contains__(item)

Return whether item is contained in this range.

Forwards to contains() with default arguments.

>>> "1.5" in SpecifierSet(">=1.0,<2.0").to_range()
True
Parameters:

item (Version | str)

Return type:

bool

__eq__(other)

Structural equality.

Compares the bounds, the === admit/reject literals, the arbitrary-string flag, the configured pre-release policy, and the opt-in region, not just the version set. Keying on the region makes equality a congruence (equal ranges stay equal under further operations), so equal implies same contains() and filter(), but not the converse: an empty range keeps the flag it was built with, so two empty ranges need not be equal.

Different specifiers for the same range fold to one canonical form:

>>> SpecifierSet(">1.0a1").to_range() == SpecifierSet(">=1.0a2.dev0").to_range()
True

The opt-in region is part of equality, so <=1.0 (no pre-releases) and <1.0.post0.dev0 (autodetects a .dev opt-in) cover the same versions yet compare unequal:

>>> le, lt = SpecifierSet("<=1.0"), SpecifierSet("<1.0.post0.dev0")
>>> le.to_range() == lt.to_range()
False
>>> r = SpecifierSet(">=1.0,<2.0").to_range()
>>> r == SpecifierSet(">=1.0,<2.0").to_range()
True
Parameters:

other (object)

Return type:

bool

__hash__()

Return hash(self).

Return type:

int

__repr__()

Human-readable representation for debugging.

>>> SpecifierSet(">=1.0,<2.0").to_range()
<VersionRange '[1.0, 2.0.dev0)'>
>>> SpecifierSet("").to_range()
<VersionRange '(-inf, +inf)' arbitrary>
>>> SpecifierSet(">=2.0,<1.0").to_range()
<VersionRange '(empty)'>
Return type:

str