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

**URL:** <https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077>\
**Category:** Packaging\
**Created:** [July 6, 2022, 1:04am UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077 "2022-07-06T01:04:42Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![harupy](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/harupy/32/7923_2.png) [@harupy](https://discuss.python.org/u/harupy)\
**Post date:** [July 6, 2022, 1:04am UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/1 "2022-07-06T01:04:42Z")

</div>

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?

---

<div class="post-metadata">

**Author:** ![abravalheri](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abravalheri/32/4990_2.png) [@abravalheri](https://discuss.python.org/u/abravalheri)\
**Post date:** [July 6, 2022, 7:18am UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/2 "2022-07-06T07:18:41Z")

</div>

> [@harupy](#):
>
> `pandas<=2.*`: Installs the latest version in `1.*`, doesn’t install the latest version in `2.*`.

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:

```auto
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](https://peps.python.org/pep-0440/#version-specifiers)

---

<div class="post-metadata">

**Author:** ![harupy](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/harupy/32/7923_2.png) [@harupy](https://discuss.python.org/u/harupy)\
**Post date:** [July 6, 2022, 7:32am UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/3 "2022-07-06T07:32:27Z")

</div>

Thanks for the comment!

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

Got it 🙂

---

<div class="post-metadata">

**Author:** ![CAM-Gerlach](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/cam-gerlach/32/3688_2.png) [@CAM-Gerlach](https://discuss.python.org/u/CAM-Gerlach)\
**Post date:** [July 6, 2022, 6:32pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/4 "2022-07-06T18:32:38Z")

</div>

Interestingly, the result of

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

```

and

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

```

are both false, even though

```python
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?

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [July 6, 2022, 8:07pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/5 "2022-07-06T20:07:51Z")

</div>

> [@CAM-Gerlach](#):
>
> any insight here?

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

---

<div class="post-metadata">

**Author:** ![abravalheri](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abravalheri/32/4990_2.png) [@abravalheri](https://discuss.python.org/u/abravalheri)\
**Post date:** [July 6, 2022, 8:51pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/6 "2022-07-06T20:51:44Z")

</div>

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`.

---

<div class="post-metadata">

**Author:** ![abravalheri](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/abravalheri/32/4990_2.png) [@abravalheri](https://discuss.python.org/u/abravalheri)\
**Post date:** [July 6, 2022, 8:58pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/7 "2022-07-06T20:58:06Z")

</div>

> [@abravalheri](#):
>
> The first comparison failing is indeed is surprising…
> 
> `2.0.0` is always less than or equal to `2.*` , regardless of which suffix you replace the `*` with.

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.

---

<div class="post-metadata">

**Author:** ![CAM-Gerlach](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/cam-gerlach/32/3688_2.png) [@CAM-Gerlach](https://discuss.python.org/u/CAM-Gerlach)\
**Post date:** [July 6, 2022, 9:13pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/8 "2022-07-06T21:13:51Z")

</div>

> [@brettcannon](#):
>
> Don’t do that?

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)?

> [@abravalheri](#):
>
> `2.4.2` is not less than or equal to `2.*` if you replace the `*` with `0.0`.

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](https://peps.python.org/pep-0440/#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](https://peps.python.org/pep-0440/#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.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [July 6, 2022, 10:40pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/9 "2022-07-06T22:40:48Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![pradyunsg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pradyunsg/32/206_2.png) [@pradyunsg](https://discuss.python.org/u/pradyunsg)\
**Post date:** [July 7, 2022, 9:09am UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/10 "2022-07-07T09:09:50Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [July 7, 2022, 7:25pm UTC](https://discuss.python.org/t/how-to-pin-a-package-to-a-specific-major-version-or-lower/17077/11 "2022-07-07T19:25:40Z")

</div>

> [@CAM-Gerlach](#):
>
> Then perhaps it should be rejected as an error to use `.*` outside of `==` comparisons

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

> [@pradyunsg](#):
>
> Alternatively, we could disallow it and have people drop the wildcard manually

I opened [Disallow `*` version modifier for anything but `==` and `!=` · Issue #566 · pypa/packaging · GitHub](https://github.com/pypa/packaging/issues/566) .
