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
Versionvalues accepted by aSpecifierSet.Construct via
to_range(), or with thefull(),empty(), andsingleton()class methods. Compose withintersection(),union(),complement(), anddifference()(or the&/|/~/-operators). Test membership withinorcontains(), filter an iterable withfilter().The configured pre-release policy of the originating specifier set carries onto the range and controls whether pre-releases are admitted under
in,contains(), andfilter(). 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 theis_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 bothcontains()andfilter(), since it was named outright.Added in version 26.3.
- static __new__(cls, *args, **kwargs)¶
- Parameters:
- Return type:
- 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:
- classmethod full(*, admit_arbitrary=True, prereleases=None)¶
Return the full range. Every PEP 440 version satisfies it.
admit_arbitrary=Falserestricts the range to PEP 440 versions only (matching the same versions asSpecifierSet(">=0.dev0").to_range()); its complement isempty(). The flag propagates through set algebra and is part of equality. DefaultTrueso thatr & full()preservesr’s own flag structurally.>>> "1.0" in VersionRange.full() True >>> "wat" in VersionRange.full() True >>> "wat" in VersionRange.full(admit_arbitrary=False) False
- Parameters:
- Return type:
- classmethod singleton(version, *, prereleases=None)¶
Return the strict singleton range
{version}.Built as the closed interval
[version, version]with strict equality.Specifier("==V")matchesV+localtoo, 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:
- intersection(other)¶
Range containing exactly the versions in both self and other.
Both operands must share the same configured pre-release policy; otherwise
ValueErroris 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:
- union(other)¶
Range containing every version in self or other.
Both operands must share the same configured pre-release policy; otherwise
ValueErroris 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:
- 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 & ~bshedb’s opt-in, so an excludedbnever force-admits a pre-release into the result. Complement stays involutive on the version set, but not on the opt-in region:~~rcovers the same versions asryet 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:
- difference(other)¶
Range containing the versions in self but not in other.
Matches
self & ~otheron the version set and the opt-in region;otheracts as a bounds-only exclusion that grants no opt-in. The arbitrary-string flag survives only whenotherremoved no versions: a difference that shrinks the bounds forgets it, asself & ~otherwould, so no later widening union can revive it. They still part on===literals, whose complement is one-way: a===literal stays whenselfadmits it andotherdoes not. Both operands must share the same configured pre-release policy (asintersection()andunion()require); otherwiseValueErroris raised.a - empty()returns a range equal toa.>>> 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:
- __and__(other)¶
Operator alias for
intersection().- Parameters:
other (object)
- Return type:
- __invert__()¶
Operator alias for
complement().- Return type:
- __sub__(other)¶
Operator alias for
difference().- Parameters:
other (object)
- Return type:
- is_subset(other)¶
Return whether every member of self is also a member of other.
On versions and
===literals this isself.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
ValueErroris 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:
- is_superset(other)¶
Return whether every member of other is also a member of self.
The mirror of
is_subset():a.is_superset(b)isb.is_subset(a).Both operands must share the same configured pre-release policy; otherwise
ValueErroris 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:
- 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
ValueErroris 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:
- 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
Nonethe 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 (asprereleases=Truewould 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']
- property is_empty: bool¶
Trueif 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:
- Return type:
Unlike
filter(), this does not consult the autodetected pre-release opt-in region; it reads only the configured policy. This mirrorscontains()versusfilter().Unparsable strings do not match, except where the full
SpecifierSetwould 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
- __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
- __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 samecontains()andfilter(), 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.devopt-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