PEP 598: Add a feature limiting capability (#1156)

The PEP's previous handling of the micro version compatibility
testing question really wasn't very good, and didn't address the
question of pickle compatibility at all.

So if this idea is going to be considered seriously, it needs
to provide a way to explicitly opt out of the feature additions,
without having to downgrade back to an earlier point release.
This commit is contained in:
Nick Coghlan 2019-09-13 11:57:20 +10:00 committed by GitHub
parent f97aa37e6a
commit ff06fa35a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 170 additions and 49 deletions

View File

@ -19,20 +19,39 @@ standard library and CPython reference interpreter by increasing the frequency
of CPython feature releases from every 18-24 months to instead occur every 9-12
months.
Adopting such an approach has several significant practical downsides, as a
CPython feature release carries certain expectations (most notably, a 5 year
maintenance lifecycle, support for parallel installation with the previous
This PEP proposes to instead *reduce* the frequency of new baseline feature
releases (with the associated filesystem layout changes, bytecode format
changes, and C ABI compatibility breaks) to occur only every other year (2020,
2022, 2024, etc), but to combine that change with a new policy and approach that
allows the introduction of backwards compatible features in the initial set of
point releases within a given release series.
In the event that this more complex proposal is *not* accepted (and either a
faster release cycle is adopted, or the status quo continues), the PEP aims to
provide useful design input for any other "Long Term Support branch"
proposals that may be put forward in the future (such as the EL Python draft at
[3_]).
Summary
=======
The proposal to keep the current CPython release compatibility management
process, but go through it more often has significant practical downsides,
as a CPython feature release carries certain expectations (most notably, a 5
year maintenance lifecycle, support for parallel installation with the previous
feature release, and the possibility of breaking changes to the CPython-specific
ABI, requiring recompilation of all extension modules) that mean faster feature
releases would significantly increase the burden of maintaining 3rd party
Python libraries and applications across all actively supported CPython
releases.
releases in their current form have the potential to significantly increase the
burden of maintaining 3rd party Python libraries and applications across all
actively supported CPython releases.
It's also arguable whether such an approach would actually noticeably reduce
the typical feature delivery latency, as the adoption cycle for new feature
releases is typically measured in months or years, so more frequent releases
may just lead to end users updating to every 3rd or 4th feature release, rather
than every 2nd or 3rd feature release (as already happens today).
It's also arguable whether such an approach would noticeably reduce the typical
feature delivery latency in practice for most end users, as the adoption cycle
for new feature releases is typically measured in months or years, so more
frequent releases may just lead to end users updating to every 3rd or 4th
feature release, rather than every 2nd or 3rd feature release (as often happens
today).
This PEP presents a competing proposal to instead *slow down* the frequency of
parallel installable feature releases that change the filesystem layout
@ -48,6 +67,12 @@ open to further incremental feature releases. Alternate implementations of
Python would also be free to clear this flag to indicate that their support for
their nominal Python version may still be a work in progress.
For compatibility testing purposes, and to maintain pickle compatibility in
mixed version environments, a new ``sys.feature_limit`` attribute (and
corresponding CPython CLI parameter, ``--feature-limit X.Y.Z``, and environment
variable, ``PYTHONFEATURELIMIT``) will provide a way to limit the runtime
availability of features added in incremental feature releases.
The existing cycle and the new cycle would be synchronised on their feature
freeze release dates, so the full Python 3.9.x feature freeze would occur in
October 2021, 24 months after the Python 3.8.0 feature release, with the initial
@ -290,6 +315,17 @@ The intent of this change in policy is to allow usability improvements for new
rather than requiring users to incur the inherent delay and costs of waiting for
and then upgrading to the next feature release series.
It is also designed such that the approval to add a feature to the next baseline
feature release can be considered separately from the question of whether or not
to make it available in the next incremental feature release for the current
release series, potentially allowing the first task to be completed by volunteer
contributors, while the latter activity could be handled by paid contributors
(e.g. customers of commercial Python redistributors could potentially request
that their vendor backport a feature, or core developers could offer to
undertake specific backports on a contract basis). (There would be potential
ethical concerns with gating bug fixes this way, but those concerns don't apply
for backports of new features)
Key characteristics of an incremental feature release:
- in an incremental feature release, ``sys.version_info.feature_complete == False``
@ -303,6 +339,11 @@ Key characteristics of an incremental feature release:
``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
- to maintain pickle compatibility in mixed version environments, and to enable
easier compatibility testing across multiple API versions within the same
release series, all API additions made in an incremental feature release
must support the new ``sys.feature_limit`` setting as described in the next
section
Key expectations around incremental feature releases:
@ -317,12 +358,103 @@ Key expectations around incremental feature releases:
in earlier versions)
Some standard library modules may also impose their own restrictions on
acceptable changes in incremental feature releases (for example, new hash
algorithms should only ever be added to ``hashlib.algorithms_guaranteed`` in
a baseline feature release - incremental feature releases would only be
acceptable changes in incremental feature releases (for example, only a
baseline feature release should ever add new hash algorithms to
``hashlib.algorithms_guaranteed`` - incremental feature releases would only be
permitted to add algorithms to ``hashlib.algorithms_available``)
Maintaining interoperability across incremental feature releases
----------------------------------------------------------------
It is a common practice to use Python's ``pickle`` module to exchange
information between Python processes running on different versions of Python.
Between release series, this compatibility is expected to only run one way
(i.e. excluding deprecated APIs, Python "X.Y+1" processes should be able to
read pickle archives produced by Python "X.Y" processes, but the reverse does
not hold, as the newer archives may reference attributes and parameters that
don't exist in the older version).
Within a release series, however, it is expected to hold in both directions,
as the "No new features" policy means that almost all pickle archives created
on Python "X.Y.Z+1" will be readable by Python "X.Y.Z" processes.
Similarly, Python libraries and applications are often only tested against
the latest version in a release series, and this is usually sufficient to keep
code working on earlier releases in that same series.
Allowing feature additions in later "X.Y.Z" releases with no way to turn them
off would pose a problem for these common practices, as a library or application
that works fine when tested on CPython version "X.Y.Z" would fail on earlier
versions if it used a feature newly introduced in "X.Y.Z", and any pickle
archives it creates that rely on those new interfaces may also not be readable
on the older versions.
To help address these problems, a new ``sys.feature_limit`` attribute would be
added, as a structured sequence corresponding to the first 3 fields in
``sys.version_info`` (``major``, ``minor``, ``micro``).
A new CLI option (``--feature-limit X.Y.Z``) and environment variable
(``PYTHONFEATURELIMIT=X.Y.Z``) would be used to set this attribute. The
``PyCoreConfig`` struct would also gain a new field::
wchar_t *feature_limit;
If the limit is not set explicitly, it would default to the first 3 fields in
``sys.version_info``. If the limit is set to a value outside the lower bound of
``sys.version_info[:2]`` and the upper bound of ``sys.version_info[:3]``, it
will be clamped to those bounds, padding with zeroes if necessary.
For example, given a current version of "3.9.3", nominal limits would be
converted to runtime ``sys.feature_limit`` values as follows::
3 => (3, 9, 0)
3.8.1 => (3, 9, 0)
3.9 => (3, 9, 0)
3.9.2 => (3, 9, 2)
<unset> => (3, 9, 3)
3.9.3 => (3, 9, 3)
3.9.4 => (3, 9, 3)
4 => (3, 9, 3)
New APIs backported to an incremental feature release would be expected to
include a guard that deletes the API from the module if the feature limit is
too low::
def feature_api():
...
_version_feature_api_added = (3, 9, 1)
if _version_feature_api_added > sys.feature_limit:
del feature_api
Similarly, new parameters would be expected to include a guard that adjusts the
function signature to match the old one::
def feature_api(old_param1, old_param2, new_param=default):
"""Updated API docstring"""
...
_version_feature_api_changed = (3, 9, 1)
if _version_feature_api_changed > sys.feature_limit:
_new_feature_api = feature_api
def feature_api(old_param1, old_param2):
"""Legacy API docstring"""
return _new_feature_api(old_param1, old_param2)
Structuring the guards this way would keep the code structure as similar as
possible between the main development branch and the backport branches, so
future bug fixes can still be backported automatically.
It is expected that convenience functions and/or additional automated tests
would eventually be added to help ensure these backported APIs are guarded
appropriately, but it seems reasonable to wait until specific concrete
examples are available to drive the design of those APIs and automated tests,
rather than designing them solely on the basis of hypothetical examples.
Feature complete release and subsequent maintenance releases
------------------------------------------------------------
@ -446,8 +578,8 @@ X.Y+1 is out, but support X.Z until X.Z+2 is out".
So this PEP takes as its starting assumption that the vast majority of Python
users simply *shouldn't need to care* that we're changing our release policy,
and the only folks that should be affected are those that are eagerly waiting
for standard library improvements, and other backwards compatible interpreter
enhancements, and those that need to manage mission critical applications in
for standard library improvements (and other backwards compatible interpreter
enhancements), and those that need to manage mission critical applications in
complex deployment environments.
@ -514,8 +646,10 @@ 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 micro version at the same time (or at least in advance of the
rollout of new higher level application versions), such that any pre-release
testing that occurs need only target a single Python micro version.
rollout of new higher level application versions), and there isn't any
requirement for older Python versions to be able to reliably read pickle streams
generated with the newer Python version, such that 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.
@ -556,38 +690,22 @@ organisations could potentially still adopt a new Python version every other
year - it would just be based on the availability of the feature complete
releases, rather than the baseline feature releases.
A less strict alternative to outright prohibition would be to restrict the use
of any release series that is still in its feature release phase to applications
where the occasional outage or failed deployment due to a lack of forwards
compatibility is considered acceptable.
A less strict alternative to outright prohibition would be to make use of the
proposed ``PYTHONFEATURELIMIT`` setting to enable phased migrations to new
incremental feature releases:
However, a third variant, which allows selective adoption of new language
features where appropriate, while also degrading gracefully enough to be
suitable for mission critical applications, would be to institute a policy that
applications wishing to target a release series that is not yet feature complete
must also support the previous feature release series for compatibility testing
purposes.
If this last policy is adopted, then testing against the previous release series
becomes the new proxy for testing against the baseline feature release and any
older incremental feature releases of the newer release series, without actually
needing to install and test against all of them.
Only after the newer release series is feature complete would support for the
previous release series be dropped entirely.
In a sufficiently complex environment, the second and third policies could also
be combined, with critical applications maintaining compatibility with the
previous release series, while newer, more experimental, applications and
services would be permitted to target the newer release series directly without
any additional safeguards in their testing process.
Depending on demand and interest, there are also further enhancements that could
be made to continuous integration pipelines to help ensure compatibility with
a chosen minimum version within a release series, without needing to run tests
against multiple micro releases. For example, applications could potentially be
tested against the latest incremental feature release, but typechecked against
the oldest still deployed incremental feature release.
* initially roll out Python X.Y.0 with ``PYTHONFEATURELIMIT=X.Y.0`` set in CI
and on deployment
* roll out Python X.Y.1 to CI, keeping the ``PYTHONFEATURELIMIT=X.Y.0`` setting
* deploy Python X.Y.1 to production based on successful CI results
* update deployment environments to set ``PYTHONFEATURELIMIT=X.Y.1``
* set ``PYTHONFEATURELIMIT=X.Y.1`` in CI only after all deployment environments
have been updated
* repeat this process for each new release up to and including the feature
complete release for the release series
* once the series is feature complete, either continue with this same process
for consistency's sake, or else stop updating ``PYTHONFEATURELIMIT`` and leave
it at the feature complete version number
Duration of the feature additions period
@ -763,6 +881,9 @@ References
.. [2] NEP proposing a standard policy for dropping support of old Python versions
(https://github.com/numpy/numpy/pull/14086)
.. [3] Draft Extended Lifecycle for Python (ELPython) design concept
(https://github.com/elpython/elpython-meta/blob/master/README.md)
Copyright
=========