Requires-python and pre-release Python versions?

It causes backsolving to an older version if any older version exists without the upper bound, which is clearly not the intent of the person placing the upper bound.

Can you give some real world examples please, I’ve never seen this with regards to Requires-Python specifically and would like to add them to know problematic scenarios in: https://github.com/notatallshaw/Pip-Resolution-Scenarios-and-Benchmarks, so they can be tracked and maybe improved upon.

I don’t have any on hand right now, as I know this is broken to use and don’t use it for things uploaded to pypi.

This specific comment mentions the issue it causes in more detail and links to more reading, Consider discarding `< 4` upper-bound on `Requires-Python` · Issue #4022 · astral-sh/uv · GitHub

I disagree with some of the conclusions of the post linked to by this linked post, but I don’t think people reaching those conclusions are necessarily wrong, but are predicting their conclusions on the current, obviously broken behavior instead of allowing that to be something that changes when considering alternatives besides telling users “don’t use this”. This is reasonable advice for the world we have, but shouldn’t be prescriptive against fixing this.

I don’t have any on hand right now, as I know this is broken to use and don’t use it for things uploaded to pypi.

This specific comment mentions the issue it causes in more detail and links to more reading, Consider discarding < 4 upper-bound on Requires-Python · Issue #4022 · astral-sh/uv · GitHub

Thanks, I was aware of that issue, but I had not clicked through to the various Poetry issues linked. As I understand it this only affects universal resolvers because they are trying to resolve for a range of Python versions, which pip is unlikely to ever support, but it will still be good to have real examples to track so if you do remember some please feel free to report an issue on that repo I linked.

Also, skimming through the Poetry issues “don’t use it for things uploaded to pypi” seems to be a theme, I can see various reports of errors where Requires-Python has caused an issue, but not reproducible examples :slightly_frowning_face:.

+1

The intent of Requires-Python is to require a minimum language version, and the language doesn’t come with prereleases. (It doesn’t even come with micro versions, but we’ve argued about that before.)

Requires-Python shouldn’t be used to try and select a particular CPython version. In practice the version numbers align, but in truth that’s coincidence (and in some ways it’s forced by the people who (incorrectly) insist that it’s truth).

2 Likes

The intent of Requires-Python is to require a minimum language version, and the language doesn’t come with prereleases. (It doesn’t even come with micro versions, but we’ve argued about that before.)

If that was the intent then the spec is badly worded, as it implies, by requiring to match the format of a Python package version specifier, which allows for micro versions and prereleases, is it’s to require a Python implementation version, not a Python language version.

And for a tool author, or a package user, a Python implementation version is far more natural as you deal with Python implementations, not Python languages.

Further, only specifying a minimum language version seems to ignore the big historical valid use case of packages supporting Python 2.7 and Python 3.4 or greater (or any specific 3.x), this required exclusions. It would be concerning to disallow that for future tools, as history often repeats itself.

1 Like

+1 on this.

However, as a procedural matter, this is without a doubt a change in the spec, and therefore would require a PEP. I’d be happy to support such a PEP, but I think we’ve pretty clearly established at this point that changing anything in this area as a “purely textual/clarification change” is going to be unhelpful. So let’s assume that any further discussion is to establish what form a PEP should take.

My personal preference for a PEP would be:

  1. Requires-Python is restricted to the forms “> M.N” or “>= M.N”. I’d accept allowing other comparisons here, but I have reservations (see below). I think we should explicitly disallow micro-version or pre-release components[1]. Note: This is a backward incompatibility, so we’d need to document what happens with older metadata that doesn’t match the new rules.
  2. Comparisons should be made against first two components of the environment’s CPython version (from sys.version). The comparison doen’t need the full semantics of the packaging version spec - a simple tuple-of-integers comparison should be fine.
  3. I’m not going to fight if the consensus is that we should use three components. But as I said above, I don’t think we should. I will fight any suggestion that we allow pre-releases.

There’s details to thrash out, but I’ll leave them to someone who is actually going to write a PEP for this.

@mikeshardmind gave an example that suggests upper limits should be included. I’m not going to object if they are, but I will point out that the example has its flaws. First of all, relying on undocumented features is always going to be dangerous - you pretty much have to check at runtime anyway, expecting package metadata to protect you is at best a first-level check. Furthermore, there’s no guarantee that other Python implementations will implement undocumented behaviour the same way anyway - so without metadata to say “this package must only be run on CPython”, you’re still open to mismatches. More generally, I don’t think the packaging ecosystem should be trying to manage details that the core language implementation isn’t willing to guarantee.

But as I said, I’m not going to object to allowing upper limits - I just don’t think they are a good idea.


  1. Constraining to that level suggests that either Python’s compatibility guarantees aren’t good enough, or the package in question shouldn’t be getting published ↩︎

3 Likes

Yes, it is. That was a mistake - after all, there’s a lot of baggage in the version spec (epoch numbers, anyone?) that isn’t applicable here.

(edit: hit “post” too soon!)

This I disagree with. If it had meant that, the text would have said “CPython version” not “Python version”. But again, conceded, it’s all imprecise so people can (and will) read whatever they want into it :slightly_frowning_face:

Exclusions are fine, and I’d support including them. Upper versions aren’t - we’ll get people specifying “<4” and being bitten when Python 4.0 is just the next version after 3.14, with no major compatibility break (which is the most likely way 4.0 will ever appear, excluding complete changes to Python’s versioning rules such as YY.MM style calver).

Upper versions are a controversial subject, though, and I’ll leave any further debate on them until there’s a PEP.

For the record, the actual package in question is not published to pypi, but I’m working on open-sourcing it (slowly removing internal company stuff from it) because of the gains this shows for asyncio + freethreading. There’s a check that happens at import for CPython, on any of windows, OSX, or linux, and only on versions that have been tested, with an explicit error if any of those conditions aren’t met, that can be converted to a warning by setting a hard to imagine would ever conflict env var name to the specific version of the library being imported.

I don’t generally recommend people rely on internals, but sometimes it ends up necessary, or you might know that you rely on a deprecated symbol in the C API and aren’t prepared with a replacement, or you might rely on something like one of the recently removed “dead batteries” and don’t have a replacement yet. Long term, I’d like to take this once public to show what would be beneficial to guarantee upstream and be out of the business of relying on something this fragile, but there are cases other than things this fragile where a known upcoming break may make sense to protect against, and that a 4 future versions out limit might be pragmatic to say “we just can’t even predict compatibility with a python version that far out and deprecation cycles”, but the current status quo makes placing that in metadata via Requires-Python a problem for library code.

Exclusions are fine, and I’d support including them. Upper versions aren’t - we’ll get people specifying “<4”

That sounds good to me, but for any future PEP on this I would strongly prefer wording that a tool may ignore parts of specifiers that were previously valid but are no longer valid, e.g >3.8,<4 the <4 part can be ignored, as not to break historical packages.

This kind of thing is understandable, but it’s not what Requires-Python is meant for.

I would definitely recommend the runtime checks, ideally without checking for specific versions but doing as much feature detection as possible (we don’t always make that possible in core, but we want to).

If you need your wheel to be rejected by the resolver at install time, use the wheel tag to indicate your supported runtimes. This will likely fall back on building from sdist, which will hopefully become non-default for regular users and will show compiler warning for aware-users.

Requires-Python is very much for telling the resolver “this package version won’t work here, but an earlier one will, so you should ignore all my files and start backtracking through my past releases to find one without this requirement”. If that isn’t already fairly clear in the docs/spec, it probably should be (I think it also makes it pretty clear why an upper bound isn’t helpful, or at least it becomes possible to infer why the upper bound isn’t helpful).

1 Like

I’m +1 on this enumeration.

I’d even say only >= M.N, > tends to act different than users think.

For the record, uv currently uses upper bounds on application requires-python to limit what is included in the lockfile (requires-python = "==3.11.*" in your python backend means we’re only locking cp311 wheels, no cp312) and that we will only find 3.11 interpreters when syncing the project, but both of those are solvable with tool-specific configuration.

Tbh the interaction between language level and cpython version is trouble anytime you want to work with alternative implementations.

1 Like

As I said above, I don’t think this is a good definition of Requires-Python, and I think we can fix it to behave how people expect rather than what happens. Forbidding a maximum version is a standards change, and this isn’t well documented in specification, it’s knowledge people pick up when things break. So why can’t we just fix this to do what users expect rather than remove it because it doesn’t?

1 Like

Because their expectations are wrong, rather than the feature?

Making Requires-Python behave sensibly with any other definition than its original one is certainly a breaking change for all existing users, and it needs clear specification for a huge number of edge cases.

The current definition just needs to behave predictably in some of those edge cases, and more clearly reject the uses that aren’t useful.

One example: the intent of the current design is for resolvers to look at the previous release of a package when the Requires-Python constraint is not met by the active version of Python. This implies that there is going to be a previous release with a different constraint. Without a time machine, this can only be a release that supported an older version of Python, because there’s no way an older release of the package could support an as-yet-unreleased new version of Python.

So an upper bound is claiming that your prior releases supported later versions of Python, which technically is possible, but what you really want in this case is to simply abort the install, not to backtrack. Changing from backtracking to aborting is a breaking change, and so we’d give it a new name and make it a new feature. Meanwhile, we’d try to adjust user’s expectations to understand that setting an upper bound with Requires-Python isn’t useful, because that doesn’t actually cause any breakage for those who are successfully using it.

1 Like

This is backwards. If I say it requires python <4, and you have python 4, I’m saying you need to go into the future from here to get to where I support 4, not the past. There should be a way to say “we dont support this version yet” so that people aren’t downloading distribution packages we know wont work.

1 Like

That would indeed be useful, but it’s not something that exists right now. Like it or not, installers look at the latest version of a package that’s available, and if the constraints on that package aren’t satisfied, it will look at older versions. That applies to all constraints, dependencies as well as Requires-Python.

I can’t even imagine how an installer that did what you suggest would work, except by never backtracking to older versions. Having a type of “cut” operator like logic programming languages, that says “if this constraint fails, stop backtracking” might be useful, but (1) it would be a new feature, needing a PEP, and (2) cuts are notoriously tricky to reason about, so I’m not convinced it would be easy to use correctly in practice.

1 Like

If it’s not feasible to put here, treating which side of a range the version someone has is outside of says which direction in time to traverse, I’m not sure what would be. Maybe 2 project level (not version level) metadata values that state the lowest version of python that there is a version that is supported, and the other being highest? traversing and downloading metadata for every version a package has by going the wrong direction in time and not accounting for why it isn’t supported ends up with a lot of wasted work.

1 Like

Would be nice, yeah, but it would also be the very first piece of project-level metadata to exist - everything else is per-file (not even per-version, though we make a lot of assumptions that metadata is per-version). I don’t think it’s a valuable enough feature to justify developing all the support it would need.

Precisely, which is why we want to ignore upper bounds on Requires-Python entirely, and make clear that’s what is happening.

The same reasoning applies to prerelease bounds. If the latest version of a package requires 3.14.0 and the version of Python is 3.14.0a1, going back to an earlier package version isn’t going to help. The two useful choices are to say that “3.14.0a1 has language version 3.14 and so meets the requirement”, or “3.14.0a1 does not meet the requirement and we should abort” except that latter part doesn’t exist and is a breaking change to add it.

The only thing we can do here is to ignore prerelease specifiers entirely on both the specification and the current version side. Or we can maintain status quo, where it’s not clearly defined and the behaviour will always be surprising. I don’t think there’s another reasonable option.

1 Like

I do not follow. Is it not plausible - likely, even - that an earlier version of a package requires eg >=3.13 and going back to that earlier version will help?

It will meet the requirement, yes. But most likely you set the requirement because something changed in 3.14 and you require it. There’s no reason to assume that change isn’t in 3.14.0a1, especially since the only people who would be using 3.14.0a1 are doing it to test that version. We don’t want to give them a “safe” old version, we want them to use the new versions and report issues.

The case where things matter here are if the package has released with a change in 3.14.0a2 but the user is still on 3.14.0a1. This is not a supported scenario (the user should update to a2), and so we don’t have to make it work, but also it’s not what Requires-Python is intended for.

This makes more sense if we move to b1 vs b2, after the feature freeze, where the language has not changed but bugs have been fixed. We don’t want packages to force users into particular prereleases - we want the latest of everything to work with prereleases. So not only does Requires-Python with a prerelease probably not do what the user wants, it doesn’t do what the core team wants and probably doesn’t do what the package developer wants.

In other words, it may be technically meaningful in some scenarios, but those scenarios are obscure, formally unsupported, and we want to discourage people from getting into them.

I think we agree on the conclusion - I find the argument that Requires-Python describes a language version and therefore that a 3.14 pre-release should “count” as 3.14 quite persuasive.

But if we decided the opposite, and that 3.14.0a1 did not satisfy >=3.14 - then your argument that trying earlier versions of the package would be unhelpful is faulty. In that world, trying earlier versions of the package that require only 3.13 language features would certainly be helpful.

Again - I actually agree with the direction that I think you are heading. But we should get there for the right reasons!