How to pin a package to a specific major version or lower

How can we pin a package to a specific major version or lower?

Let’s say we want to pin pandas to 2.* or lower. How can we achieve this?

  • pandas<=2.*: Installs the latest version in 1.*, doesn’t install the latest version in 2.*.
  • pandas<3: This seems to work. Is this the only way?

To include only versions in the 2.* range, I belive you can also use the match operator or the compatible release operator. So all of the following should work similarly:

pandas >=2, <3

pandas == 2.*

pandas ~= 2.0

If you want to also include the 0.* and/or the 1.* range, than the exclusive ordered comparison (pandas < 3) is the way to go, because commas are treated as an AND operator.

Reference: PEP 440 – Version Identification and Dependency Specification | peps.python.org

2 Likes

Thanks for the comment!

the exclusive ordered comparison (pandas < 3 ) is the way to go

Got it :slight_smile:

Interestingly, the result of

packaging.version.Version("2.0.0") in packaging.specifiers.SpecifierSet("<=2.*")

and

packaging.version.Version("2.4.2") in packaging.specifiers.SpecifierSet("<=2.*")

are both false, even though

packaging.version.Version("2.0.0") in packaging.specifiers.SpecifierSet("<=2")

is true (though of course, "2.0.1" is false). From my reading, PEP 440 unclear on whether the wildcard is valid with non == comparison operators, and if so the precise meaning, but intuitively if it causes any following version components to be ignored, the first two examples should be True; otherwise, if is not valid, it should simply be an error rather than returning what appears to be an unexpected result.

@brettcannon @ncoghlan any insight here?

1 Like

Don’t do that? :wink: Seriously, I don’t know how to interpret <=2.* since 2.* is a moving target as it matches a varying number of versions.

The first comparison failing is indeed is surprising…

I would think that in the expression x <= 2.*, x has to be less than or equal to any version representable by 2.*,

2.0.0 is always less than or equal to 2.*, regardless of which suffix you replace the * with.
2.4.2 is not less than or equal to 2.* if you replace the * with 0.0.

Or maybe not …
2.* could also mean 2.0b0 right? If that is the case 2.0 does not satisfy the expression x <= 2.*.


But I agree that a failure would make more sense.

Then perhaps it should be rejected as an error to use .* outside of == comparisons, since likely most user using it will not get the results they expect (as @harupy did above, and I did in my own testing)?

Well, it depends what one means by “equal to”. I’d expect it to have the same meaning as the == operator, in which case 2.4.2 would satisfy x == 2.*.

Furthermore, the definition of the wildcard operator in the PEP states

Prefix matching may be requested instead of strict comparison, by appending a trailing .* to the version identifier in the version matching clause. This means that additional trailing segments will be ignored when determining whether or not a version identifier matches the clause. If the specified version includes only a release segment, than trailing components (or the lack thereof) in the release segment are also ignored.

Following this definition, ignoring trailing segments after the first release segment, 2, 2.4.2 satisfies this constraint, as the comparison becomes 2 <= 2 which is of course True.

However, given that, evident from this discussion, even people quite familiar with the specs still interpret it differently, it would indeed seem prudent to simply make use of prefix matching with operators other than == and =! an explicit error. Indeed, per a strict reading of the existing PEP prefix matching is only valid for the == and =! operators, as they state, respectively:

The specified version identifier must be in the standard format described in Version scheme, but a trailing .* is permitted on public version identifiers as described below.

The allowed version identifiers and comparison semantics are the same as those of the Version matching operator, except that the sense of any match is inverted.

While the others state that only “version identifiers” are permitted, or (in the case of ===) explicitly prohibit prefix matching:

A compatible release clause consists of the compatible release operator ~= and a version identifier.

An inclusive ordered comparison clause includes a comparison operator and a version identifier

Arbitrary equality comparisons are simple string equality operations which do not take into account any of the semantic information such as zero padding or local versions. This operator also does not support prefix matching as the == operator does.

2 Likes

Christopher already made the response I was going to make: for PEP 440 as written, using wildcards outside of “==” and “!=” comparisons isn’t valid.

Allowing them for “>=” and “<=” would be reasonable, but it would involve a PEP to fix the spec. (It wasn’t a conscious choice to exclude them, we just didn’t notice at the time that the inclusive ordered comparison operators weren’t formally defined as combining an exclusive ordered comparison with a version match, so the tools have been written to ignore the wildcard instead of paying attention to it)

Making a coherent definition wouldn’t be too hard: just ignore the wildcard for the exclusive ordered comparison part and keep it for the version matching part.

Alternatively, we could disallow it and have people drop the wildcard manually — that weirdness was recently discussed, but my search foo is failing me.

3 Likes

I opened Disallow `*` version modifier for anything but `==` and `!=` · Issue #566 · pypa/packaging · GitHub .

3 Likes