PEP 2026: Calendar versioning for Python

As a relatively new user I’m thinking about what a “release” really means in the context of Python. The question when I see a new version of Python is whether my script/app/library will work under that version of python, and in my mental model a Python release (well, CPython release to be exact) entails at least the following conceptually-distinct “components” with varying levels of “promises” (formal or informal) on compatibility:

  1. Python language features (such as new syntax) — this is basically always backwards-compatible by design, minus some odd cases
  2. Standard library and API — changes in this respect are mostly backwards-compatible, but both the API and behavior can and do have breaking changes if given very good reasons
  3. (C) ABI — while I’m not skillful enough to write C code, plenty of dependencies probably use C and rely on the stability of the ABI, and it seems reasonable for me to assume things might break if the ABI is changed in any way and my deps haven’t been update to handle this.
  4. CPython interpreter — an implementation detail about which the compatibility I as a user probably shouldn’t be worried too much (besides performance upgrades :slightly_smiling_face:), and (hopefully) my deps don’t get to rely on implementation details too much.

(this is without looking at the typing / async / packaging changes which follow their own schedules)

Assuming my mental model isn’t too far from reality, I’m wondering how tightly coupled these “components” are within a release, and how useful the notion of “annual” release is in describing our situation if these components have different levels of granularity in their update schedule. Like, the interpreter always changes in bugfixes, and we sometimes get API improvements between major releases, but we don’t get language changes that often if at all (never mind the very occasional breaking syntax changes that everyone’s worried about and generate the most headlines as in the case of 2->3 transition :smiling_face_with_tear:).

To me as a user, the most mentally intuitive (not necessarily the objective best) versioning scheme is one that reflects the (loose or tight) level of update coupling between these components. Linux distros by definition are a collection of kernel, libs and apps each with its own update policy, and CalVer does seem to me to make the most sense in those cases (since no good notion of compatibility or feature update exists). How well this applies to the case of CPython is another question.

4 Likes

This is actually a pretty good mental model (even to the point where the pure Python code in the standard library is often shared directly across different Python implementations, just as many Linux distro components are common across distros). We even have precedent for bundling snapshots of externally developed components (i.e. ensurepip).

However, if you trawl through the history of rejected and withdrawn PEPs, you’ll find some proposals that attempted to decouple the standard library versioning from the core CPython reference interpreter versioning.

It’s one of those ideas that sounds like it should work until you sit down and try to specify a way that it could work and end up going “Wow, that’s seriously confusing”. The most significant problem with trying to do this is that the boundary between the reference interpreter and the standard library is fuzzy. Some things are obviously on one side or the other, but things get really blurry when standard library modules are directly invoked by the interpreter (e.g. builtins, runpy, asyncio, importlib), or are exposing or accessing interpreter internals like sys, gc, threading, types, weakref, etc.

With the standard library version and the CPython reference interpreter version being the same number, the only folks that really need to care about the boundary are interpreter implementers, and they can adopt a very pragmatic definition: if they can use the CPython code as is, then it’s part of the standard library, if they have to write their own, then it’s part of the reference interpeter. If we try to separate the version numbers, then everyone has to care where the boundary lies.

(and we should probably stop this tangent here or else split it out to its own thread in the Ideas or Help categories, since it is getting borderline off-topic for this PEP’s thread)

4 Likes

Thanks for the clarification. If I understand correctly, the tight-coupling between API/CPython is effectively tying these components (and to a lesser extent, occasionally ABI) into roughly the same release cadence (which happens to be annual). If this is the case (and if there are no plans to make CPython a looser collection of components), then as a user for one I don’t have strong opinions for or against CalVer.

1 Like

If Python is going to adopt CalVer I do think YYYY.N.M makes the most sense[1]. Based on this discussion, it doesn’t feel like 3.YY will be that helpful for people who don’t already know all of the background.

Switching to that format is more disruptive for the reasons the PEP outlines, but it’s a case of “mild disruption for ~no benefit” versus “less-mild disruption for small benefit”.


  1. assuming N.M corresponds to e.g. 13.1, not 3.13 ↩︎

6 Likes

Considering the idea of YYYY.MM.nn versioning further, some of the specific disruptions can be eliminated by making the stable ABI versioning independent of the reported Python runtime version, and switching some other things to be based on the stable ABI version rather than the Python runtime version. (These ideas would also apply to YYYY.serial_within_year.maintenance, since the version info fields are still just called major, minor, micro rather than being renamed to year, month, micro)

The concerns raised in PEP 2026 – Calendar versioning for Python | peps.python.org and PEP 641 – Using an underscore in the version portion of Python 3.10 compatibility tags | peps.python.org are taken into account below, but some of them would come up again in a few decades when 2100.10.0 was imminent (but adding the required disambiguating underscores could happen anytime before then).

Edits:

  • changed the definition of sys.cpython_abi.max_version to provide a nicer backwards compatibility workaround than str(sys.version_info.major)[-2:] for the fields where switching from 2 to 4 digits would be problematic.
  • added details on CPython C API implications (with a further change to sys.cpython_abi.max_version to bump the values in the major and minor fields down to the minor and micro fields to make room to set max_version.major to the ABI generation)

Hypothetical CPython changes

  • CPython changes sys.version_info to put the release year in the major field and the release month in the minor field. The fields names are left unchanged since they’re common across implementations.

  • CPython continues to make sys.implementation.version an alias for sys.version_info (i.e. the major, minor, micro field names are retained, rather than attempting to rename the first two fields to year and month)

  • CPython adds sys.cpython_abi with the following fields (see below for rationale):

    • generation: CPython stable ABI version number (currently always 3)

    • version: max CPython stable ABI version that is supported by this runtime. Extension modules that use functions from later major versions of even the same CPython ABI generation aren’t expected to work since symbols they need will be missing. Even on CPython, this would NOT be the same as sys.version_info:

      • major: always the same value as sys.cpython_abi.generation
      • minor: set to sys.version_info.major - 2000 on CPython (so it won’t go over two digits until 2100)
      • micro: set to sys.version_info.minor on CPython
      • releaselevel: updated for YYYY.MM.0 pre-releases, but locked at "final" after that point
      • serial: updated for YYYY.MM.0 pre-releases, but locked at 0 after that point
  • the idea behind having a cpython_ prefix on the new attribute is to let other implementations explicitly declare their support for the CPython stable ABI (if any) independently of their support for Python runtime APIs. More details on that below.

  • CPython’s C API versioning would diverge from its Python runtime API versioning:

    • The existing Py_*_VERSION macros would map to the corresponding sys.cpython_abi.max_version fields rather than to sys.version_info
    • Py_RUNTIME_*_VERSION fields would be added that retained the current meaning of the Py_*_VERSION macros (mapping to sys.version_info)
  • the sys.version string would be updated to have a new “CPython ABI” section appended to the end (e.g. (CPython ABI: 3.26.10)) when sys.cpython_abi is not None)

  • the pythonN executable name would be f"python{sys.cpython_abi.generation} rather than the current f"python{sys.version_info.major} (i.e. python3 remains the versioned binary name rather than it becoming python2026)

  • the pythonN.X executable name would be f"python{sys.cpython_abi.max_version.major}.{python{sys.cpython_abi.max_version.minor}" rather than the current f"python{sys.version_info.major}.{sys.version_info.minor} (i.e python3.26 instead of python3.15, python3.2026 or python2026.10)

  • sys.implementation.cache_tag would be f"cpython-{sys.cpython_abi.max_version.major}{sys.cpython_abi.max_version.minor}" rather than the current f"cpython-{sys.version_info.major}.{sys.version_info.minor}" (i.e cpython-326 instead of cpython-315, cpython-32026, cpython-3_2026, cpython-202610 or cpython-2026_10)

  • similarly, the SOABI config var would start with cpython-326 instead of cpython-315 or any of the other potential combinations

  • any other case where {sys.version_info.major} and {sys.version_info.minor} are combined to produce a string would similarly be replaced with the combination of {sys.cpython_abi.max_version.major} and {sys.cpython_abi.max_version.minor}

  • properly dealing with the year 2100 would likely require finally introducing disambiguating underscores sometime in the next few decades, but the above scheme is designed to automatically use 3100 in affected fields if nothing changes (just as it would in 2111 when we reached 3.100 with the current versioning scheme and release cadence).

  • handling all of the above situations while allowing for multiple releases per year would be left as a question for a multi-release-per-year PEP to address (if anyone ever advocates for that), as it would require either adding sys.cpython_abi.max_version.micro info to affected fields in some way, or else continuing to restrict ABI changes to one release per year, even if runtime feature releases became more frequent (with the latter approach, releases without ABI changes would report the ABI version info for the relevant ABI release in sys.cpython_abi.max_version rather than following the scheme described above).

Interpreting sys.cpython_abi

This hypothetical new field is added to allow the following interpretations:

  • if sys.cpython_abi is missing, then checks for CPython ABI support should fall back to the status quo of looking at sys.implementation.name and sys.version_info or inspecting the values of relevant sysconfig vars.
  • if sys.cpython_abi is set to None, the implementation is explicitly stating that CPython extension modules are not supported and won’t work
  • if sys.cpython_abi is set and not None, then extension modules for cp{sys.cpython_abi.generation}-abi{sys.cpython_abi.generation} are expected to work, as are versioned stable modules up to cp{sys.cpython_abi.version.major}{sys.cpython_abi.max_version.minor}-abi{sys.cpython_abi.generation}

Preserving C API compatibility means that some field needs to go away (just as the leading 3 would get dropped from sys.version_info), but the right one to drop for ABI versioning purposes is the always 0 micro version rather than the 3 version prefix. This reflects the fact that CPython micro releases aren’t permitted to break ABI compatibility, and are even discouraged from making additions to the ABI due to the potential for extension module portability problems.

ABI generations

In the above sketch, I suggest adding sys.cpython_abi.generation to describe the current abi3 generation, rather than attempting to reuse the word version. This is mostly a matter of choosing and explicitly documenting an alternative term that is “like a version, but different” so that the distinction between the Python version and the ABI generation is clearer. The word epoch is avoided, since that already means something else in package versioning.

It would also be reasonable to refer to this as a “Python generation”, but that seems more likely to bring back bad Python 2->3 transitions memories than calling it an ABI generation.

The new field is technically redundant (since sys.cpython_abi.generation and sys.cpython_abi.max_version.major are defined as having the same value), but I think the dedicated generation attribute conveys the significance better than merely defining sys.cpython_abi_version with the same structure as sys.version_info would.

Backwards compatibility

C API

Breaking the direct link between the build time CPython API/ABI versioning and the CPython runtime versioning definitely has the potential to cause problems if extension modules that don’t use the stable ABI are expecting the build time values of the Py_*_VERSION macros to always match the runtime contents of sys.version_info.

These can already differ for modules using the stable ABI, so those modules shouldn’t encounter any new problems along those lines, and modules affected by the problem would need changes anyway if they were ever ported to the stable ABI.

The transition could potentially be made smoother by introducing the Py_RUNTIME_*_VERSION macros and sys.cpython_abi interface in CPython 3.14 (while still using the current incremental versioning scheme), with the only semantic change being that the values of Py_MICRO_VERSION, Py_RELEASE_LEVEL, and Py_RELEASE_SERIAL (and their corresponding fields in sys.cpython_abi) wouldn’t change after the 3.14.0 release.

Runtime version checks

The assessment of YY.0 from PEP 2026 still applies for YYYY.MM.0

Checks for major Python versions that are greater than 2, or not exactly 2 will still work as expected when the major version is in the thousands instead of being exactly 3.

Checks specifically for sys.version_info.major == 3 or sys.version.startswith("3.") will break, but have always been semantically dubious and actively discouraged.

String slicing on sys.version would already have run into problems with previous releases, either with the 2.7.10 release back in 2015 (if attempting to retrieve all fields), or else with the 3.9 to 3.10 version change which meant the length of the version as a string could now vary from 5 characters (x.y.z for versions prior to 3.10), up to 7 characters (x.yy.zz for maintenance releases of versions after 3.9 later in their lifecycle, starting with ‘3.10.10’ in Feb 2023). This means that any such code would have already broken, and needed to switch to either accessing sys.version_info or else handling variable length fields in their version string parsing. It isn’t a new problem being introduced by considering a switch to CalVer.

Packaging ecosystem

The hypothetical approach described above is intentionally designed to mimic the way the 3.9 to 3.10 transition was handled via the workaround of defining the CPython ABI’s sys.cpython_abi.max_version.minor as the CPython runtime’s sys.version_info.major - 2000 rather than using the full 4-digit year.

PEP 641 already described the right way to fix the overall ambiguity in version numbers: it’s a matter of including underscores where appropriate.

While the hypothetical approach addresses most potential problems, there are still genuine compatibility issues when it comes to code that attempts to predict how a CPython release will behave without actually running it. Such code will make guesses like cpython-202610 for the SOABI prefix based on the existing CPython logic unless it is updated to take the concept of CPython ABI generations into account.

My own thoughts

I think this still qualifies as “more trouble than it would be worth”, but I do feel it’s important to compare 3.YY.x to the most compatible approach to YYYY.MM.x that we can devise, rather than comparing it to the more simplistic approaches that really would break the world when it came to package installation (I’m less concerned about trying to keep incorrectly structured runtime version checks “working”).

If we went down this path, we would at least get the genuine benefit of decoupling the extension module ABI compatibility versioning from the Python runtime versioning, so implementations other than CPython could accurately report their support for the CPython stable ABI (if they offer it).

5 Likes

This feels like a more beneficial path, and I would be fairly supportive of rebranding abi3/abi4/etc. to py2024/py2034 (or wherever it starts), which is fairly in line with other languages in our ecosystem.

The big difference from this proposal is that we wouldn’t rev it every year, because we don’t want to be changing[/breaking] the ABI every year. It still leaves the language free to change more often, since that’s what people[1] want, but provided we also don’t break anything in the language over that time period it’s a useful marker for pure-Python compatibility as well.

I guess my argument is more that annual feature/breaking releases are the problem, not the version number. (Probably unsurprising, since I put up the alternative release PEP with @ncoghlan… maybe that explains both of our reservations about this whole proposal.)


  1. Contributors, not users :wink: ↩︎

8 Likes

What practical problems does this this PEP solve?

Python seems to follow semver in spirit, even if not precisely. I much prefer that because it tells me something about the release. Furthermore, I prefer it over dated versions precisely because it decouples functionality from dates.

Even if calendar versioning were deemed better, I am skeptical that it’s worth the effort and confusion that will ensue.

5 Likes

The PEP solves for the fact some of us think Python does not follow SemVer and people think it does (i.e. I disagree with your view that we follow SemVer in any way :sweat_smile:).

3 Likes

This does raise an important question. Switching to CalVer or any other versioning scheme is the kind of change that we generally only have one chance of getting it nice. If the situation from PEP 602 changes and Python were to adapt its release frequency from annual to shorter or longer (say, if we have “summer” and “winter” releases), the versioning scheme shall leave room for this possibility gracefully (that is, without incurring the mental overload of overly long strings like 3.260430). There will be quite a bit of disruption, particularly if we decide to drop the major version number (making it 2026.1.0) which won’t play nice with the questionable practice of specifying version upper bounds <4 (IIRC this used to be in some packaging tutorial / guidance some years ago), decisions need to be made very carefully.

Python is never SemVer, SemVer is not generally reflective of what Python is doing. Python having so many moving parts with varying degrees of coupling, the notion of compatibility simply cannot be flattened into a 3-tuple.

And perhaps more

Add free-threading to the mix and you get a matrix, but I digress :slightly_smiling_face:

Upon reading a new release of some software I usually think of 3 questions:

  1. Should I upgrade?
  2. How to upgrade?
  3. When to upgrade?

SemVer answers (1) and (2) as in “this release is a drop-in enhancement” but is too simplistic for such projects as Python.

CalVer partly answers (3) as in “I’m 2 years out of date” but deliberately leaves (1) and (2) unanswered.

For (2) we can’t rely on Python the language staying on 3 and assume the upgrade will be painless either, since API, ABI and behavior changes are possible.

Breaking the false impression of SemVer can be done in ways other than a potentially disruptive switch to another versioning scheme. What’s more important is the collective effort, for which we have already got a great community, to help navigate this kind of “{whether / when / how} to upgrade” questions.

2 Likes

I’m going to disagree here. CalVer tells you when something was released, but it does not tell you when it is out of date. Taking Ubuntu versions as an example, there’s no obvious distinction between release 22.04 (released April 2022) and release 23.04 (released April 2023), and yet they will become outdated at VERY different times - 23.04 was supported for just under a year, reaching end of life in Jan 2024, but 22.04 is a Long Term Support release that will continue to be supported until 2029.

6 Likes

This is the last I’ll say about it in this thread, but the idea of decoupling the stable ABI versioning from the CPython runtime versioning feels like it has merit regardless of whether CPython switches its runtime version numbering to a different scheme.

It would be especially valuable if it allowed for transitional releases where CPython supported a new stable ABI generation without dropping the old one.

Anyway, I’ll keep pondering for a while and eventually post something in a new Ideas thread (with a pointer from the C API Discord channel)

6 Likes

The New Stack seems to be reporting based on the PSF blog post and references YuppieScum on Slashdot:

Did Y2K teach us nothing?
How the hell did nobody ask “Is using just two digits for the year a good idea?” again ?

Indeed, YuppieScum, this has been asked many times, and is addressed right there in the PEP’s spec:

  • YY is the minor version number - it is the short year number: {year} - 2000.

[…]
In the year 2100, the minor will be 2100-2000 = 100, therefore the version will be 3.100.0.

I’m not sure what we can put in the PEP to help prevent YuppieScum’s misconceptions when I doubt they read the PEP in the first place :slight_smile:


Hmm, I think it’s hard for us to make assumptions about what will be obvious or not after we’ve been using any given scheme for 86 years :wink:


A fun idea, but this RM isn’t volunteering for it :upside_down_face:


Python doesn’t follow SemVer in spirit. If Python adopted SemVer, that would imply a new major bump every year when we remove deprecations. The last major bump was in 2008. With PEP 602, Python releases are very much tied to the calendar.


Yeah, it doesn’t tell you when something is out of date as such, but gives an idea how far you are. For example, if a project is still using a 2015 version, that tells you more than 3.5.

Hmm, that’s true for Ubuntu, but Python doesn’t use LTS (or: all versions are LTS!), and so there’s no such huge difference: 2022 and 2023 releases become outdated in 2027 and 2028 respectively. I however do see your point that this isn’t implicit in CalVer itself and depends on your release schedule.

3 Likes

What do others think about 3.YYYY.micro?

It would be require more changes for compared with 3.YY, but they don’t look too big at first glance (I’ve made a quick CPython 3.2026 build and tests pass), especially when compared with YY.x or YYYY.x, and we’d get the full year like 3.2026 which many people desire.

Related, thanks @ncoghlan for writing up the proposal for YYYY.x in PEP 2026: Calendar versioning for Python - #67 by ncoghlan. What do people think of this?

2 Likes

I think the suggestion was that it follows the spirit of “the version number changes when the interface changes”, as opposed to “the version number changes when the date changes”.

If all someone gets out of SemVer is “the first field indicates deprecations” then they’ve sadly missed the intent (or if you like, the spirit) of SemVer so badly that they probably shouldn’t bring it up.

Treating version numbers as compatibility identifiers is far more like SemVer than CalVer. Treating them like best-before dates is more like CalVer. Bad analogies are easy, so it’s always worth looking past the badness to try and see what is actually being expressed.

7 Likes

True, but not very much more without knowing the release cadence. If I’m using version 3.8 and someone else is using 3.5, I know there’s three versions’ worth of difference between us. If I’m using Fortran 77 and he’s using Fortran 95, how many versions are there?

Right. I was considering using two completely different CalVer projects to highlight the distinction but Ubuntu provided both in one easy package :slight_smile:

I brought up the question about what to do in the year 2100 at the Language Summit, so I’m still theoretically in favor of a 4-digit middle number. The question is, do we have data to back up the rejection rational in PEP 2026? How big would such a break actually be?

Clearly that’s a problem for the Priests of the Temple of Syrinx to solve. And if they can’t, someone else is going to assume control.

3 Likes

Ruff’s YTT1 rules looks for code that is likely already broken for 3.9 → 3.10 due to assuming the minor is always one digit. Searching the top 8,000 PyPI projects (downloaded 2024-04-14) reveals 27 instances of YTT101 (sys.version[:3]), zero of YTT102 (sys.version[2]) and 20 of YTT103 (sys.version compared to string).

This is code that would be broken for 3.YYYY, but is already broken for 3.10.

Summary
228:AWSIoTPythonSDK-1.5.4/AWSIoTPythonSDK/core/protocol/paho/client.py:601:12: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
312:Cython-3.0.10/runtests.py:2919:67: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
1045:RPi.GPIO-0.7.1/test/test.py:43:4: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
1828:ansible-9.4.0/ansible_collections/netapp/ontap/plugins/modules/na_ontap_debug.py:74:53: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
1856:asyncio-3.4.3/runtests.py:40:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
2140:boto-2.49.0/boto/connection.py:657:36: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
2141:boto-2.49.0/boto/connection.py:658:21: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
2313:click-configfile-0.2.3/setup.py:97:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3196:distribute-0.7.3/pkg_resources.py:222:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3197:distribute-0.7.3/setuptools/command/__init__.py:11:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3198:distribute-0.7.3/setuptools/command/bdist_egg.py:310:17: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3199:distribute-0.7.3/setuptools/command/bdist_egg.py:455:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3200:distribute-0.7.3/setuptools/command/bdist_egg.py:541:21: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3201:distribute-0.7.3/setuptools/command/bdist_rpm.py:15:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3202:distribute-0.7.3/setuptools/command/bdist_rpm.py:22:59: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3203:distribute-0.7.3/setuptools/command/build_py.py:182:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3204:distribute-0.7.3/setuptools/command/easy_install.py:69:21: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3205:distribute-0.7.3/setuptools/command/easy_install.py:1400:53: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3206:distribute-0.7.3/setuptools/command/easy_install.py:1418:42: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3207:distribute-0.7.3/setuptools/package_index.py:195:5: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3473:future-1.0.0/src/future/backports/urllib/request.py:150:15: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3474:future-1.0.0/src/future/backports/xmlrpc/client.py:165:15: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3483:gnupg-2.3.1/setup.py:51:8: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3508:instana-1.38.2/tests/platforms/test_lambda.py:235:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3509:instana-1.38.2/tests/platforms/test_lambda.py:301:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3510:instana-1.38.2/tests/platforms/test_lambda.py:367:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3511:instana-1.38.2/tests/platforms/test_lambda.py:433:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
3512:intelhex-2.3.0/intelhex/test.py:1574:40: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3513:ixnetwork_restpy-1.1.12/ixnetwork_restpy/connection.py:121:16: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
3514:ixnetwork_restpy-1.1.12/uhd_restpy/connection.py:121:16: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
4611:librosa-0.10.1/tests/test_convert.py:594:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
4612:librosa-0.10.1/tests/test_convert.py:671:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
4613:librosa-0.10.1/tests/test_notation.py:183:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
4657:lmdb-1.4.1/setup.py:48:4: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
4741:mechanize-0.4.9/mechanize/_urllib2_fork.py:104:15: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
4742:mitogen-0.3.7/mitogen/parent.py:1449:72: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
5241:ncclient-0.6.15/ncclient/transport/third_party/junos/ioproc.py:9:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
5565:nvidia-ml-py-12.535.133/setup.py:6:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
5566:nvidia-ml-py3-7.352.0/setup.py:5:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
6865:pex-2.3.1/pex/vendor/_vendored/pip/pip/_vendor/distlib/_backport/sysconfig.py:498:12: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
6993:pony-0.7.17/pony/orm/decompiling.py:1030:8: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
9556:uiautomation-2.0.18/setup.py:10:4: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`
9901:uncompyle6-3.9.1/test/stdlib/compile-file-1x.py:14:14: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
10386:wxPython-4.2.1/build.py:287:16: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
10387:wxPython-4.2.1/build.py:291:21: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
11340:wxPython-4.2.1/wx/lib/activexwrapper.py:27:12: YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`
11351:z3-solver-4.13.0.0/core/scripts/mk_util.py:2823:8: YTT103 `sys.version` compared to string (python3.10), use `sys.version_info`

Extending Ruff to add rules searching for sys.version[:4] or sys.version[:5] finds nothing.

Grepping for any sys.version slicing found 95 matching lines in 37 projects (two of these are Ruff and the Flake8 plugin’s YTT rules :slight_smile:):

Summary
❯ python3 ~/github/misc/cpython/search_pypi_top.py -q . "sys\.version\[\d*:\d*\]"
./dbus-python-1.3.2.tar.gz: dbus-python-1.3.2/aclocal.m4: dnl sys.version[:3], printing that failed with Python 3.10, since the
./dbus-python-1.3.2.tar.gz: dbus-python-1.3.2/configure: if python_implementation() == 'CPython' and sys.version[:3] == '2.7':
./distribute-0.7.3.zip: distribute-0.7.3/pkg_resources.py: PY_MAJOR = sys.version[:3]
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/package_index.py: sys.version[:3], require('setuptools')[0].version
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/command/bdist_egg.py: pyver = sys.version[:3]
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/command/bdist_egg.py: if sys.version[:3]=="2.4":  # -m works w/zipfiles in 2.5
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/command/bdist_rpm.py: src.endswith('.src.rpm') and 'any' or sys.version[:3],
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/command/easy_install.py: "python" + sys.version[:3],
./distribute-0.7.3.zip: distribute-0.7.3/setuptools/command/easy_install.py: sys.version[:3],
./astroquery-0.4.7.tar.gz: astroquery-0.4.7/astropy_helpers/astropy_helpers/utils.py: plat_specifier = '.{0}-{1}'.format(cmd.plat_name, sys.version[0:3])
./conda-4.3.16.tar.gz: conda-4.3.16/conda/cli/main_info.py: print("sys.version: %s..." % (sys.version[:40]))
./ed25519-1.5.tar.gz: ed25519-1.5/setup.py: plat_specifier = ".%s-%s" % (self.plat_name, sys.version[0:3])
./boto-2.49.0.tar.gz: boto-2.49.0/boto/connection.py: if ((ON_APP_ENGINE and sys.version[:3] == '2.5') or
./boto-2.49.0.tar.gz: boto-2.49.0/boto/connection.py: sys.version[:3] in ('2.6', '2.7')) and port == 443:
./Cython-3.0.10.tar.gz: Cython-3.0.10/Demos/embed/Makefile: PYVERSION := $(shell $(PYTHON) -c "import sys; print(sys.version[:3])")
./Cython-3.0.10.tar.gz: Cython-3.0.10/runtests.py: sys_pyregr_dir = os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test')
./future-1.0.0.tar.gz: future-1.0.0/src/future/backports/urllib/request.py: __version__ = sys.version[:3]
./future-1.0.0.tar.gz: future-1.0.0/src/future/backports/xmlrpc/client.py: __version__ = sys.version[:3]
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/PKG-INFO: | YTT101 | `sys.version[:3]` referenced (python3.10)              |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/PKG-INFO: | YTT303 | `sys.version[:1]` referenced (python10)                |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/PKG-INFO: python_version = sys.version[:3]  # YTT101
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/PKG-INFO: if sys.version[:1] >= '3':  # YTT303
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/README.md: | YTT101 | `sys.version[:3]` referenced (python3.10)              |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/README.md: | YTT303 | `sys.version[:1]` referenced (python10)                |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/README.md: python_version = sys.version[:3]  # YTT101
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/README.md: if sys.version[:1] >= '3':  # YTT303
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.egg-info/PKG-INFO: | YTT101 | `sys.version[:3]` referenced (python3.10)              |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.egg-info/PKG-INFO: | YTT303 | `sys.version[:1]` referenced (python10)                |
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.egg-info/PKG-INFO: python_version = sys.version[:3]  # YTT101
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.egg-info/PKG-INFO: if sys.version[:1] >= '3':  # YTT303
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.py: YTT101 = 'YTT101 `sys.version[:3]` referenced (python3.10), use `sys.version_info`'  # noqa: E501
./flake8_2020-1.8.1.tar.gz: flake8_2020-1.8.1/flake8_2020.py: YTT303 = 'YTT303 `sys.version[:1]` referenced (python10), use `sys.version_info`'  # noqa: E501
./cartoframes-1.2.5.tar.gz: cartoframes-1.2.5/tests/e2e/data/services/test_geocoding.py: pyver = sys.version[0:3].replace('.', '_')
./cartoframes-1.2.5.tar.gz: cartoframes-1.2.5/tests/e2e/data/services/test_isolines.py: pyver = sys.version[0:3].replace('.', '_')
./gnupg-2.3.1.tar.gz: gnupg-2.3.1/setup.py: if sys.version[:3] == "2.6":
./imageio-2.34.0.tar.gz: imageio-2.34.0/imageio/plugins/_tifffile.py: if float(sys.version[0:3]) < 2.7:
./json_tricks-3.17.3.tar.gz: json_tricks-3.17.3/json_tricks/encoders.py: if (sys.version[:2] == '2.') else 'this should not happen in Python3'))
./google-apitools-0.5.32.tar.gz: google-apitools-0.5.32/ez_setup.py: DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
./google-apitools-0.5.32.tar.gz: google-apitools-0.5.32/ez_setup.py: egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
./instana-1.38.2.tar.gz: instana-1.38.2/tests/platforms/test_lambda.py: if sys.version[:3] == '2.7':
./instana-1.38.2.tar.gz: instana-1.38.2/tests/platforms/test_lambda.py: if sys.version[:3] == '2.7':
./instana-1.38.2.tar.gz: instana-1.38.2/tests/platforms/test_lambda.py: if sys.version[:3] == '2.7':
./instana-1.38.2.tar.gz: instana-1.38.2/tests/platforms/test_lambda.py: if sys.version[:3] == '2.7':
./ansible-9.4.0.tar.gz: ansible-9.4.0/ansible_collections/netapp/ontap/plugins/modules/na_ontap_debug.py: self.log_list.append('Python version: %s' % sys.version[:3])
./catboost-1.2.3.tar.gz: catboost-1.2.3/catboost_all_src/contrib/python/numpy/py2/numpy/distutils/command/build.py: plat_specifier = ".%s-%s" % (get_platform(), sys.version[0:3])
./catboost-1.2.3.tar.gz: catboost-1.2.3/catboost_all_src/contrib/python/numpy/py2/numpy/distutils/command/build_src.py: plat_specifier = ".%s-%s" % (get_platform(), sys.version[0:3])
./mitogen-0.3.7.tar.gz: mitogen-0.3.7/mitogen/parent.py: if os.uname()[0]=='Darwin'and os.uname()[2][:2]in'2021'and sys.version[:3]=='2.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1'
./anyconfig-0.14.0.tar.gz: anyconfig-0.14.0/pkg/package.spec.in: tox -e py$(python -c "import sys; sys.stdout.write(sys.version[:3].replace('.', ''))")
./lmdb-1.4.1.tar.gz: lmdb-1.4.1/setup.py: if sys.version[:3] < '2.7' or (3, 0) < sys.version_info[:2] < (3, 4):
./pony-0.7.17.tar.gz: pony-0.7.17/pony/orm/decompiling.py: if sys.version[:3] > '2.4': outmost_iterable_name = '.0'
./mechanize-0.4.9.tar.gz: mechanize-0.4.9/mechanize/_urllib2_fork.py: __version__ = sys.version[:3]
./pycrypto-2.6.1.tar.gz: pycrypto-2.6.1/setup.py: if sys.version[0:1] == '1':
./python-gflags-3.1.2.tar.gz: python-gflags-3.1.2/debian/rules: #PYVER   := $(shell $(PYTHON) -c 'import sys; print sys.version[:3]')
./pygments-2.17.2.tar.gz: pygments-2.17.2/tests/examplefiles/make/Makefile: $(RUNSHARED) ./$(BUILDPYTHON) -E -c 'import sys ; from distutils.util import get_platform ; print get_platform()+"-"+sys.version[0:3]' >platform
./pygments-2.17.2.tar.gz: pygments-2.17.2/tests/examplefiles/make/Makefile.output: '\'import sys ; from distutils.util import get_platform ; print get_platform()+"-"+sys.version[0:3]\'' Literal.String.Single
./Pylons-1.0.3.tar.gz: Pylons-1.0.3/pylons/templates/default_project/ez_setup.py: DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
./Pylons-1.0.3.tar.gz: Pylons-1.0.3/pylons/templates/default_project/ez_setup.py: egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
./Pylons-1.0.3.tar.gz: Pylons-1.0.3/pylons/templates/minimal_project/ez_setup.py: DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
./Pylons-1.0.3.tar.gz: Pylons-1.0.3/pylons/templates/minimal_project/ez_setup.py: egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
./pyarmor-8.5.2.zip: pyarmor-8.5.2/pyarmor/packer.py: 'build', 'exe.%s-%s' % (get_platform(), sys.version[0:3])),
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: /// Checks for uses of `sys.version[:3]`.
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: /// `sys.version[:3]` will truncate the version number (e.g., `"3.10"` would
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: /// sys.version[:3]  # Evaluates to "3.1" on Python 3.10.
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: /// Checks for uses of `sys.version[:1]`.
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: /// sys.version[:1]  # If using Python 10, this evaluates to "1".
./ruff-0.3.7.tar.gz: ruff-0.3.7/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs: format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
./ruyaml-0.91.0.tar.gz: ruyaml-0.91.0/_test/lib/test_build.py: 'build', 'lib.%s-%s' % (distutils.util.get_platform(), sys.version[0:3])
./ruyaml-0.91.0.tar.gz: ruyaml-0.91.0/_test/lib/test_build_ext.py: 'build', 'lib.%s-%s' % (distutils.util.get_platform(), sys.version[0:3])
./spark-0.2.1.tar.gz: spark-0.2.1/spark/contribs/ez_setup.py: DEFAULT_URL     = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
./spark-0.2.1.tar.gz: spark-0.2.1/spark/contribs/ez_setup.py: egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
./spark_parser-1.8.9.tar.gz: spark_parser-1.8.9/spark_parser/spark.py: if sys.version[0:3] <= '2.3':
./RPi.GPIO-0.7.1.tar.gz: RPi.GPIO-0.7.1/test/test.py: if sys.version[:3] == '2.6':
./scikit_image-0.23.1.tar.gz: scikit_image-0.23.1/skimage/_shared/version_requirements.py: actver = sys.version[:6]
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py27/lib64/python2.7/config/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print get_platform()+"-"+sys.version[0:3]' >platform
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py27/lib/python2.7/config/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print get_platform()+"-"+sys.version[0:3]' >platform
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py35/lib64/python3.5/config-3.5m/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print(get_platform()+"-"+sys.version[0:3])' >platform
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py35/lib/python3.5/config-3.5m/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print(get_platform()+"-"+sys.version[0:3])' >platform
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py34/lib64/python3.4/config-3.4m/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print(get_platform()+"-"+sys.version[0:3])' >platform
./odfpy-1.4.1.tar.gz: odfpy-1.4.1/.tox/py34/lib/python3.4/config-3.4m/Makefile: $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print(get_platform()+"-"+sys.version[0:3])' >platform
./wxPython-4.2.1.tar.gz: wxPython-4.2.1/wx/lib/activexwrapper.py: if sys.version[:3] >= '2.4' and not os.path.exists(dllpath):
./wxPython-4.2.1.tar.gz: wxPython-4.2.1/build.py: PYVER = runcmd([PYTHON, '-c', 'import sys; print(sys.version[:3])'],
./wxPython-4.2.1.tar.gz: wxPython-4.2.1/build.py: if sys.version[:3] != PYVER:
./wxPython-4.2.1.tar.gz: wxPython-4.2.1/build.py: PYVER = sys.version[:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/setup.py: % sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/setup.py: % sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/setup.py: % sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/setup.py: % sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/setup.py: "\nThis package is not supported for Python version %s." % sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/test/add-test.py: version = sys.version[0:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/test/stdlib/compile-file-1x.py: PY_VERSION = sys.version[:3]
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/uncompyle6/scanner.py: # scanner = get_scanner(sys.version[:5], False)
./uncompyle6-3.9.1.tar.gz: uncompyle6-3.9.1/uncompyle6/semantics/aligner.py: version = float(sys.version[0:3])
./xdis-6.1.0.tar.gz: xdis-6.1.0/test/bytecompile-tests: version = sys.version[:3]
./xdis-6.1.0.tar.gz: xdis-6.1.0/test_unit/test_load.py: self.assertEqual(sys.version[0:3], str(version))

Time: 0:00:30.874332
Found 95 matching lines in 37 projects

Most of these are slicing up to the first three characters, which a change to 3.xxxx wouldn’t affect:

  • sys\.version\[\d*:\d*\]: Found 95 matching lines in 37 projects
  • sys\.version\[[0-2]?:[1-3]\]: Found 92 matching lines in 35 projects

We would have to adjust PY_VERSION_HEX to take a minor > 255. I think this would be okay, as it’s intended for numeric comparisons, e.g. #if PY_VERSION_HEX >= .... In the top 8k, I can only one instance of it being bit shifted (hexversion >> 16 != PY_VERSION_HEX >> 16). Any thoughts on this?

1 Like

I didn’t go through every example, but the results don’t seem insurmountable to me.

Good call out with PY_VERSION_HEX. That should be add to the PEP.

2 Likes

I’m actually surprised at how few packages are affected, but I guess 3.10 probably shook out a lot of bad practices.

I’m curious how often code is checking if sys.version[0] == '3' or if sys.version_info[0] == 3. I guess that question is similar to “how many maintained packages still try to support python 2?”. I guess someone out there might be guarding against python4 showing up but I kinda doubt it…

It never occurred to me that pip version numbers were actually year numbers. I think people are just used to seeing a version number and not thinking twice about what it means.

10 Likes