Design Discussion
Why rolling pre-freeze releases over simply doing more frequent X.Y.0 releases?
For large parts of Python’s user base, availability of new CPython feature
releases isn’t the limiting factor on their adoption of those new releases
(this effect is visible in such metrics as PyPI download metadata).
As such, any proposal based on speeding up full feature releases needs to strike
a balance between meeting the needs of users who would be adopting each release
as it became available, and those that would now be in a position of adopting
every 2nd, 3rd, or 4th release, rather than being able to migrate to almost
every release at some point within its lifecycle.
This proposal aims to approach the problem from a different angle by defining a
new production-ready release stream that is more specifically tailored to the
interests of operating environments that are able to consume new releases as
fast as the CPython core team is prepared to produce them.
Is it necessary to keep the “alpha” and “beta” naming scheme?
Using the “a” and “b” initials for the proposed rolling releases is a design
constraint imposed by some of the pragmatic aspects of the way CPython version
numbers are published.
Specifically, alpha releases, beta releases, and release candidates are reported
in some places using the strings “a”, “b”, and “c” respectively, while in others
they’re reported using the hex digits 0xA
, 0xB
, and 0xC
. We want to
preserve that, while also ensuring that any Python-Requires
constraints
are expressed against the beta releases rather than the alpha releases (since
the latter may not enforce the abi3
stability requirements if two alpha
releases occur in succession).
However, there isn’t anything forcing us to say that the “a” stands for “alpha”
or the “b” stands for “beta”.
That means that if we wanted to increase adoption amongst folks that were
only being put off by the “beta” label, then it may make sense to emphasise
the “*A*BI breaking” and “*B*inary compatible” names over the “alpha”
and “beta” names, giving:
- 3.9.0a1: ABI breaking pre-freeze release
- 3.9.0b2: binary compatible pre-freeze release
- 3.9.0rc1: release candidate
- 3.9.0: final release
This iteration of the PEP doesn’t go that far, as limiting initial adoption
of the rolling pre-freeze releases to folks that are comfortable with the
“beta” label is likely to be a good thing, as it is the early adopters of these
releases that are going to encounter any unexpected consequences that occur
at the level of the wider Python ecosystem, and we’re going to need them to
be willing to take an active part in getting those issues resolved.
Moving away from the “beta” naming would then become an option to keep in mind
for the future, assuming the resulting experience is sufficiently positive that
we decide the approach is worth continuing.
Why rolling pre-freeze releases rather than alternating between stable and unstable release series?
Rather than using the beta period for rolling releases, another option would be
to alternate between traditional stable releases (for 3.8.x, 3.10.x, etc), and
release series that used the new rolling release cadence (for 3.9.x, 3.11.x,
etc).
This idea suffers from the same core problem as PEP 598 and PEP 602: it imposes
changes on end users that are happy with the status quo without offering them
any clear compensating benefit.
It’s also affected by one of the main concerns raised against PEP 598: at least
some core developers and end users strongly prefer that no particular semantics
be assigned to the value of any of the numbers in a release version. These
community members instead prefer that all the semantic significance be
associated with the position within the release number that is changing.
By contrast, the rolling pre-freeze release proposal aims to address that concern by
ensuring that the proposed changes in policy all revolve around whether a
particular release is an alpha release, beta release, release candidate, or
final release.
Why not use Calendar Versioning for the rolling release stream?
Steve Dower’s initial write-up of this proposal [1_] suggested the use of
calendar versioning for the rolling release stream (so the first rolling
pre-release after Python 3.8.0 would have been Python 2019.12 rather than
3.9.0b1).
Paul Moore pointed out [2_] two major practical problems with that proposal:
- it isn’t going to be clear to users of the calendar-based versions where they
stand in relation to the traditionally numbered versions - it breaks
Python-Requires
metadata processing in packaging tools with
no clear way of fixing it reliably (since all calendar versions would appear
as newer than any standard version)
This PEP aims to address both of those problems by using the established beta
version numbers for the rolling releases.
As an example, consider the following question: “Does Python 2021.12 include
all the new features released in Python 3.9.0?”. With calendar versioning on
the rolling releases, that’s impossible to answer without consulting a release
calendar to see when 3.9.0rc1 was branched off from the rolling release series.
By contrast, the equivalent question for rolling pre-freeze releases is
straightforward to answer: “Does Python 3.10.0b2 include all the new features
released in Python 3.9.0?”. Just from formulating the question, the answer is
clearly “Yes, unless they were provisional features that got removed”.
The beta numbering approach also avoids other questions raised by the calendar
versioning concept, such as how sys.version_info
, PY_VERSION_HEX
,
site-packages
directory naming, and installed Python binary and extension
module naming would work.
How would users of the rolling pre-freeze releases detect API changes?
When adding new features, core developers would be strongly encouraged to
support feature detection and graceful fallback to alternative approaches via
mechanisms that don’t rely on either sys.version_info
or runtime code object
introspection.
In most cases, a simple hasattr
check on the affected module will serve this
purpose, but when it doesn’t, alternative approaches would be considered as part
of the feature addition. Prior art in this area includes the
pickle.HIGHEST_PROTOCOL
attribute, the hashlib.algorithms_available
set,
and the various os.supports_*
sets that the os
module already offers for
platform dependent capability detection.
It would also be possible to add features that need to be explicitly enabled
via a __future__
import when first included in the rolling pre-freeze releases,
even if that feature flag was subsequently enabled by default before its first
appearance in an X.Y.0 release candidate.
The rationale behind these approaches is that explicit detection/enabling like
this would make it straightforward for users of the rolling pre-freeze release
stream to notice when we remove or change provisional features
(e.g. from __future__
imports break on compile if the feature flag no
longer exists), or to safely fall back on previous functionality.
The interpreter’s rich attribute lookup machinery means we can also choose to
add warnings for provisional or deprecated imports and attributes that we don’t
have any practical way to add for checks against the value of
sys.version_info
.
Why add a new pre-freeze ABI flag to force rebuilds after X.Y.0rc1?
The core development team currently actively discourage the creation of
public pre-built binaries for an X.Y series prior to the ABI freeze date.
The reason we do that is to avoid the risk of painful debugging sessions
on the stable X.Y.0 release that get traced back to “Oh, our dependency
‘superfast-binary-operation’ was affected by a CPython ABI break in
X.Y.0a3, but the project hasn’t published a new build since then”.
With the proposed pre-freeze ABI flag in place, this aspect of the
release adoption process continues on essentially unchanged from the
status quo: a new CPython X.Y release series hits ABI freeze -> package
maintainers publish new binary extension modules for that release
series -> end users only get segfaults due to actual bugs, not just
builds against an incompatible ABI.
The primary goal of the new pre-freeze ABI flag is then to improve
the user experience of the rolling pre-freeze releases themselves, by
allowing pre-built binary archives to be published for those releases
without risking the problems that currently cause us to actively
discourage the publication of binary artifacts prior to ABI freeze.
In the ideal case, package maintainers will only need to publish
one pre-freeze binary build at X.Y.0a1, and then a post-freeze
build after X.Y.0rc1. The only situations that should require
a rebuild in the meantime are those where the project was
actually affected by a CPython ABI break in an intervening alpha
release.
As a concrete example, consider the scenario where we end up having three
releases that include ABI breaks: X.Y.0a1, X.Y.0a5, X.Y.0a7. The X.Y.0a7 ABI is
then the ABI that carries through all the subsequent beta releases and into
X.Y.0rc1. (This is the scenario illustrated in figure 1)
Forcing everyone to rebuild the world every time there’s an alpha release in
the rolling release stream would almost certainly lead to publishers deciding
supporting the rolling releases was more trouble than it was worth, so we want
to allow modules built against X.Y.0a1 to be loaded against X.Y.0a7, as they’re
probably going to be compatible (there are very few projects that use every
C API that CPython publishes, and most ABI breaks affect a single specific API).
Once we publish X.Y.0rc1 though, we want to ensure that any binaries that were
built against X.Y.0a1 and X.Y.0a4 are completely removed from the end user
experience. It would be nice to be able to keep the builds against X.Y.0a7 and
any subsequent beta releases (since it turned out those actually were built
against the post-freeze ABI, even if we didn’t know that at the time), but
losing them isn’t any worse than the status quo.
This means that the pre-freeze flag is “the simplest thing that could possibly
work” to solve this problem - it’s just a new ABI flag, and we already have
the tools available to deal with ABI flags (both in the interpreter and in
package publication and installation tools).
Since the ABI flags have changed relative to the pre-releases, projects don’t
even need to publish a new release: they can upload new wheel archives to their
existing releases, just as they can today.
A cleverer scheme that was able to retroactively accept everything built
against the last alpha or subsequent beta releases would likely be possible,
but it isn’t considered necessary for adoption of this PEP, as even if we
initially start out with a simple pre-release ABI flag, it would still be
possible to devise a more sophisticated approach in the future.
Implications for CPython core development
The major change for CPython core development is the need to keep the master
branch more consistently release ready.
While the main requirement for that would be to keep the stable BuildBot fleet
green, there would also be encouragement to keep the development version of
the documentation up to date for the benefit of users of the rolling pre-freeze
releases. This will include providing draft What’s New entries for changes as
they are implemented, although the initial versions may be relatively sparse,
and then expanded based on feedback from beta release users.
For core developers working on the CPython C API, there would also be a new
requirement to consistently mark ABI breaking changes in their NEWS file
snippets.
On the specific topic of the stable ABI, most API designs will be able to go
through a process where they’re first introduced as a provisional part of the
full CPython API (allowing changes between pre-freeze releases), and only
promoted to the stable ABI once developers are confident that the interface
is genuinely stable.
It’s only in rare cases where an API serves no useful purpose outside the
stable ABI that it may make sense to publish an alpha release containing a
provisional stable ABI addition rather than iterating on the design in the
provisional CPython API instead.
Implications for Python library development
If this PEP is successful in its aims, then supporting the rolling pre-freeze
release stream shouldn’t be subtantially more painful for library authors than
supporting the stable releases.
For publishers of pure Python packages, this would be a matter of publishing
“py3” tagged wheel archives, and potentially adding the rolling pre-freeze
release stream to their test matrix if that option is available to them.
For publishers of binary extension modules, the preferred option would be to
target the stable C ABI (if feasible), and thus enjoy an experience similar to
that of pure Python packages, where a single pre-built wheel archive is able to
cover multiple versions of Python, including the rolling pre-freeze release
stream.
This option isn’t going to be viable for all libraries, and the desired outcome
for those authors is that they be able to support the rolling releases by
building and publishing one additional wheel archive, built against the initial
X.Y.0a1 release. The subsequent build against X.Y.0rc1 or later is then the same
build that would have been needed if only supporting the final stable release.
Additional wheel builds beyond those two should then only be needed if that
particular library is directly affected by an ABI break in any other alpha
release that occurs between those two points.
Having a rolling pre-freeze release stream available may also make it more feasible
for more CI providers to offer a “CPython beta release” testing option. At the
moment, this feature is only available from CI providers that are willing and
able to put the necessary time and effort into creating, testing, and publishing
their own builds from the CPython master branch (e.g. [6_]).
Implications for the proposed Scientific Python ecosystem support period
Based on discussions at SciPy 2019, NEP (NumPy Enhancement Proposal) 29 has
been drafted [3_] to propose a common convention across the Scientific Python
ecosystem for dropping support for older Python versions.
While the exact formulation of that policy is still being discussed, the initial
proposal is very simple: support any Python feature release published within
the last 42 months.
For an 18 month feature release cadence, that works out to always supporting at
least the two most recent feature releases, and then dropping support for all
X.Y.Z releases around 6 months after X.(Y+2).0 is released. This means there is
a 6 month period roughly every other year where the three most recent feature
releases are supported.
For a 12 month release cadence, it would work out to always supporting at
least the three most recent feature releases, and then dropping support for all
X.Y.Z releases around 6 months after X.(Y+3).0 is released. This means that
for half of each year, the four most recent feature releases would be supported.
For a 24 month release cadence, a 42 month support cycle works out to always
supporting at least the most recent feature release, and then dropping support
for all X.Y.Z releases around 18 months after X.(Y+1).0 is released.
This means there is a 6 month period every other year where only one feature
release is supported. Under the proposal in this PEP, that period would
correspond to the final few rolling pre-freeze releases and the release candidate
phase for the upcoming stable feature release.
Release cycle alignment for core development sprints
With the proposal in this PEP, it is expected that the focus of core
development sprints would shift slightly based on the current location
in the two year cycle.
In release years, the timing of PyCon US is suitable for new contributors to
work on bug fixes and smaller features before the first release candidate goes
out, while the Language Summit and core developer discussions can focus on
plans for the next release series.
The pre-alpha core development sprint in release years will provide an
opportunity to incorporate feedback received on the previous release, either
as part of the next maintenance release (for bug fixes and feedback on
provisional APIs), or as part of the first alpha release of the next release
series (for feedback received on stable APIs).
Those initial alpha releases would also be the preferred target for ABI breaking
changes to the full CPython ABI (while changes later in the release cycle
would still be permitted as described in this PEP, landing them in the X.Y.0a1
release means that they won’t trigger any additional work for publishers of
pre-built binary packages).
The Steering Council elections for the next release cycle are also likely to
occur around the same time as the pre-alpha development sprints.
In non-release years, the focus for both events would just be on the upcoming
maintenance and pre-freeze releases. These less intense years would hopefully
provide an opportunity to tackle various process changes and infrastructure
upgrades without impacting the release candidate preparation process.
Release cycle alignment for prominent Linux distributions
Some rolling release Linux distributions (e.g. Arch, Gentoo) may be in a
position to consume the new rolling pre-freeze releases proposed in this PEP,
but it is expected that most distributions would continue to use the established
releases.
The specific dates for final releases proposed in this PEP are chosen to align
with the feature freeze schedules for the annual October releases of the Ubuntu
and Fedora Linux distributions.
For both Fedora and Ubuntu, it means that the release candidate phase aligns
with the development period for a distro release, which is the ideal time for
them to test a new version and provide feedback on potential regressions and
compatibility concerns.
For Ubuntu, this also means that their April LTS releases will have benefited
from a full short-term release cycle using the new system Python version, while
still having that CPython release be open to upstream bug fixes for most of the
time until the next Ubuntu LTS release.
The one Linux release cycle alignment that is likely to be consistently poor
with the specific proposal in this PEP is with Debian, as that has been released
in the first half of odd-numbered years since 2005 (roughly 12 months offset
from Ubuntu LTS releases).
With the annual release proposal in PEP 602, both Debian and Ubuntu LTS would
consistently get a system Python version that is around 6 months old, but
would also consistently select different Python versions from each other.
With a two year cadence, and CPython releases in the latter half of the year,
they’re likely to select the same version as each other, but one of them will
be choosing a CPython release that is more than 18 months behind the latest beta
releases by the time the Linux distribution ships.
If that situation does occur, and is deemed undesirable (but not sufficiently
undesirable for Debian to choose to adjust their release timing), then that’s
where the additional complexity of the “incremental feature release” proposal
in PEP 598 may prove worthwhile.
(Moving CPython releases to the same half of the year as the Debian and Ubuntu
LTS releases would potentially help mitigate the problem, but also creates
new problems where a slip in the CPython release schedule could directly affect
the release schedule for a Linux distribution, or else result in a distribution
shipping a Python version that is more than 18 months old)
Implications for simple deployment environments
For the purposes of this PEP, a “simple” deployment environment is any use case
where it is straightforward to ensure that all target environments are updated
to a new Python release at the same time (or at least in advance of the rollout
of new higher level application versions), and any pre-release testing that
occurs need only target a single Python micro version.
The simplest such case would be scripting for personal use, where the testing
and target environments are the exact same environment.
Similarly simple environments would be containerised web services, where the
same Python container is used in the CI pipeline as is used on deployment, and
any application that bundles its own Python runtime, rather than relying on a
pre-existing Python deployment on the target system.
For these use cases, there is a straightforward mechanism to minimise the
impact of this PEP: continue using the stable releases, and ignore the rolling
pre-freeze releases.
To actually adopt the rolling pre-freeze releases in these environments, the
main challenge will be handling the potential for extension module segfaults
when the next pre-freeze release is an alpha release rather than a beta
release, indicating that the CPython ABI may have changed in an incompatible
way.
If all extension modules in use target the stable ABI, then there’s no problem,
and everything will work just as smoothly as it does on the stable releases.
Alternatively, “rebuild and recache all extension modules” could become a
standard activity undertaken as part of updating to an alpha release.
Finally, it would also be reasonable to just not worry about it until something
actually breaks, and then handle it like any other library compatibility issue
found in a new alpha or beta release.
Aside from extension module ABI compatibilty, the other main point of additional
complexity when using the rolling pre-freeze releases would be “roll-back”
compatibility for independently versioned features, such as pickle and SQLite,
where use of new or provisional features in the beta stream may create files
that are not readable by the stable release. Applications that use these
kinds of features and also require the ability to reliably roll-back to a
previous stable CPython release would, as today, be advised to avoid adopting
pre-release versions.
Implications for complex deployment environments
For the purposes of this PEP, “complex” deployment environments are use cases
which don’t meet the “simple deployment” criteria above. They may involve
multiple distinct versions of Python, use of a personalised build of Python,
or “gatekeepers” who are required to approve use of a new version prior to
deployment.
For example, organisations that install Python on their users’ machines as part
of a standard operating environment fall into this category, as do those that
provide a standard build environment. Distributions such as conda-forge or
WinPython that provide collections of consistently built and verified packages
are impacted in similar ways.
These organisations tend to either prefer high stability (for example, all of
those who are happily using the system Python in a stable Linux distribution
like Debian, RHEL/CentOS, or Ubuntu LTS as their preferred Python environment)
or fast turnaround (for example, those who regularly contribute toward the
latest CPython pre-releases).
In some cases, both usage models may exist within the same organisation for
different purposes, such as:
- using a stable Python environment for mission critical systems, but allowing
data scientists to use the latest available version for ad hoc data anaylsis - a hardware manufacturer deploying a stable Python version as part of their
production firmware, but using the latest available version in the development
and execution of their automated integration tests
Under any release model, each new release of Python generates work for these
organisations. This work may involve legal, security or technical reviews of
Python itself, assessment and verification of impactful changes, reapplication
of patches, recompilation and testing of third-party dependencies, and
only then deployment.
Organisations that can take updates quickly should be able to make use of the
more frequent beta releases. While each update will still require similar
investigative work to what they require today, the volume of work required per
release should be reduced as each release will be more similar to the previous
than it is under the present model. One advantage of the proposed
release-every-2-months model is that organisations can choose their own adoption
cadence from adopting every beta release, to adopting one per quarter, or one
every 6 months, or one every year. Beyond that, it would likely make more sense
to continue using the stable releases instead.
For organisations with stricter evaluations or a preference for stability, the
longer release cycle for stable releases will reduce the annual effort required
to update, the longer release candidate period will allow more time to do
internal testing before the X.Y.0 release, and the greater use by others
during the beta period will provide more confidence in the initial releases.
Meanwhile, the organisation can confidently upgrade through maintenance
releases for a longer time without fear of breaking changes.
Acknowledgements
Thanks to Łukasz Langa for creating PEP 602 and prompting this discussion of
possible improvements to the CPython release cadence, and to Kyle Stanley
and h-vetinari for constructive feedback on the initial draft of this PEP.
References
… [1] Steve Dower’s initial “Fast and Stable releases” proposal
(PEP 602: Annual Release Cycle for Python)
… [2] Paul Moore’s initial comments on Steve’s proposal
(PEP 602: Annual Release Cycle for Python)
… [3] NEP 29 proposes a common policy for dropping support of old Python versions
(https://numpy.org/neps/nep-0029-deprecation_policy.html)
… [4] Example implementation for a pre-release SOABI flag
(https://github.com/ncoghlan/cpython/pull/3)
… [5] CPython stable ABI documentation
(https://docs.python.org/3/c-api/stable.html)
… [6] Travis CI nightly CPython builds
(https://docs.travis-ci.com/user/languages/python/#nightly-build-support)
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal
license, whichever is more permissive.