PEP 387: backwards compatibilty policy

The steering council thinks it’s about time to make a decision on this PEP and our official policy on backwards compatibility. Benjamin has made a couple minor updates at the SC’s request (i.e. updating the PEP for our annual release cadence, having deprecations specify the targeted removal date).

PEP: 387
Title: Backwards Compatibility Policy
Version: $Revision$
Last-Modified: $Date$
Author: Benjamin Peterson <benjamin@python.org>
BDFL-Delegate: Brett Cannon (on behalf of the steering council)
Discussions-To: https://discuss.python.org/t/pep-387-backwards-compatibilty-policy/
Status: Draft
Type: Process
Content-Type: text/x-rst
Created: 18-Jun-2009
Post-History: 19-Jun-2009, 12-Jun-2020


Abstract
========

This PEP outlines Python's backwards compatibility policy.


Rationale
=========

As one of the most used programming languages today [#tiobe]_, the
Python core language and its standard library play a critical role in
millions of applications and libraries. This is fantastic. However, it
means the development team must be very careful not to break this
existing 3rd party code with new releases.

This PEP takes the perspective that "backwards incompatibility" means
preexisting code ceases to comparatively function after a change. It is
acknowledged that this is not a concrete definition, but the expectation
is people in general understand what is meant by
"backwards incompatibility", and if they are unsure they may ask the
Python development team and/or steering council for guidance.


Backwards Compatibility Rules
=============================

This policy applies to all public APIs.  These include:

- Syntax and behavior of these constructs as defined by the reference
  manual.

- The C-API.

- Function, class, module, attribute, and method names and types.

- Given a set of arguments, the return value, side effects, and raised
  exceptions of a function.  This does not preclude changes from
  reasonable bug fixes.

- The position and expected types of arguments and returned values.

- Behavior of classes with regards to subclasses: the conditions under
  which overridden methods are called.

- Documented exceptions and the semantics which lead to their raising.

- Exceptions commonly raised in EAFP scenarios.

Others are explicitly not part of the public API.  They can change or
be removed at any time in any way.  These include:

- Function, class, module, attribute, method, and C-API names and
  types that are prefixed by "_" (except special names).

- Anything documented publicly as being private.

- Imported modules (unless explicitly documented as part of the public
  API; e.g. importing the ``bacon`` module in the ``spam`` does not
  automatically mean ``spam.bacon`` is part of the public API unless
  it is documented as such).

- Inheritance patterns of internal classes.

- Test suites.  (Anything in the ``Lib/test`` directory or test
  subdirectories of packages.)


Basic policy for backwards compatibility
----------------------------------------

* In general, incompatibilities should have a large benefit to
  breakage ratio, and the incompatibility should be easy to resolve in
  affected code.  For example, adding an stdlib module with the same
  name as a third party package is generally not acceptable.  Adding
  a method or attribute that conflicts with 3rd party code through
  inheritance, however, is likely reasonable.

* Unless it is going through the deprecation process below, the
  behavior of an API *must* not change in an incompatible fashion
  between any two consecutive releases.  Python's yearly release
  process (:pep:`602`) means that the deprecation period must last at
  least two years.

* Similarly a feature cannot be removed without notice between any two
  consecutive releases.

* The steering council may grant exceptions to this policy. In
  particular, they may shorten the required deprecation period for a
  feature. Exceptions are only granted for extreme situations such as
  dangerously broken or insecure features or features no one could
  reasonably be depending on (e.g., support for completely obsolete
  platforms).


Making Incompatible Changes
===========================

Making an incompatible change is a gradual process performed over
several releases:

1. Discuss the change.  Depending on the degree of incompatibility,
   this could be on the bug tracker, python-dev, python-list, or the
   appropriate SIG.  A PEP or similar document may be written.
   Hopefully users of the affected API will pipe up to comment.

2. Add a warning.  If behavior is changing, the API may gain a new
   function or method to perform the new behavior; old usage should
   raise the warning.  If an API is being removed, simply warn
   whenever it is entered.  ``DeprecationWarning`` is the usual
   warning category to use, but ``PendingDeprecationWarning`` may be
   used in special cases were the old and new versions of the API will
   coexist for many releases [#warnings]_. Compiler warnings are also
   acceptable. The warning message should include the release the
   incompatibility is expected to become the default and a link to an
   issue that users can post feedback to.

3. Wait for the warning to appear in at least two major Python
   versions. It's fine to wait more than two releases.

4. See if there's any feedback.  Users not involved in the original
   discussions may comment now after seeing the warning.  Perhaps
   reconsider.

5. The behavior change or feature removal may now be made default or
   permanent having reached the declared version. Remove the old
   version and warning.


References
==========

.. [#tiobe] TIOBE Programming Community Index

   http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

.. [#warnings] The warnings module

   http://docs.python.org/library/warnings.html


Copyright
=========

This document has been placed in the public domain.



..
   Local Variables:
   mode: indented-text
   indent-tabs-mode: nil
   sentence-end-double-space: t
   fill-column: 70
   coding: utf-8
   End:

4 Likes

The PEP may need a special exemption for modules that have external dependencies and are bound to the feature set of external libraries. The ssl modules comes to my mind…

4 Likes

I’d be wary of adding any absolute prohibitions. And this one in particular is problematic by being out of the team’s control. It’s e.g. not impossible if stdlib incorporates a 3rd-party module – AFAIK multiprocessing is such a module, – or a 3rd-party module squats a particularly common word.

4 Likes

What constitutes a “change”? The doc is full of “added in vX”/“changed in vX” notes. Is this now prohibited, only new API entries? COM has a tradition of interfaces like “Foo_4” due to such a requirement there.

What’s the rationale? Is this a compromise of some kind with those who think that 2 years is not enough and were sabotaging current deprecations or something?

I fully agree about the quoted section being a bit problematic for name squatting, and it might also be an issue for inactive projects. If the conflicting 3rd party module is not a real, actively maintained project (admittedly, this is difficult to formally define) then it should not be considered for the purposes of name collision.

I suspect this would have to be determined on a case-by-case basis by the Steering Council. This is technically covered by “The steering council may grant exceptions to this policy”, but it might be better to explicitly state something like this:

“For example, adding an stdlib module with the same name as an actively maintained third party package is not acceptable.”

Perhaps with a footnote which explains that determining whether a third party package is “actively maintained” is decided on a case-by-case basis.

Just making it a non-absolute prohibition will do: “is typically not acceptable”.

The PEP says:

“Given a set of arguments, the return value, side effects, and
raised exceptions of a function. This does not preclude changes from
reasonable bug fixes.”

That reads like a suggestion that any exception that might be raised
should always be considered a part of the public API. I don’t think we
want that. Traditionally, Python functions don’t document the full set
of exceptions that could possibly be raised, because that would be “all
of them”.

Can the PEP clarify the following?

  • documented exceptions MUST be treated as part of the public API,
    and therefore subject to backwards compatibility requirements;

  • undocumented exceptions which are commonly used in “EAFP” style
    code SHOULD be so treated;

  • other undocumented exceptions MAY be so treated;

  • but “incidental” exceptions that merely happen to occur due to
    bugs or invalid use SHOULD NOT be so treated.

Where capitalised terms MUST, SHOULD, MAY and SHOULD NOT are to be
interpreted as in RFC 2119.

http://ietf.org/rfc/rfc2119.txt

The PEP says:

“Others are explicitly not part of the public API. They can change or
be removed at any time in any way. These include:”

I think it will be useful to avoid future arguments if the PEP is more
explicit about cases that are not part of the public API:

  • anything documented as private, even if it lacks a leading underscore;
    (that includes docstrings, but not mere source code comments)

  • any imported module, unless explicit documented as public;

  • in the event of a conflict, where an entity is documented as both
    private and public, public takes priority.

To clarify, suppose I have a function without a leading underscore, that
would default to public.

If I merely add a comment # Private function to the source code, where
users cannot be expected to see it, that’s not sufficient to break the
backwards compatibility requirement.

But if I stick that comment in the docstring of the function or its
module, or document a restriction that anything not otherwise documented
as public should be treated as private, that takes priority.

A module might document that only functions that are listed in __all__
are public and that everything else is an implementation detail that
cannot be relied on. That should be honoured without forcing the module
to add leading underscores.

The PEP says:

“adding an stdlib module with the same name as a third party package is
not acceptable.”

Others have mentioned namesquatters and abandoned packages, but we
should also be explicit that we only care about public third-party
packages. Even if only for the practical reason that it is impossible
for us to do due diligence that module names aren’t already being used
in private.

If we want to add a new std lib module “widgetlib”, the existence of
someone’s private “widgetlib” should not block us from using that name.

(Although, in practice, if someone like Google were to ask nicely
“please don’t use that name, because we’re already using it internally”
we’d probably comply. But that’s the privilege of size, power and
money.)

1 Like

An additional comment on the PEP:

“Unless it is going through the deprecation process below, the behavior
of an API must not change between any two consecutive releases.
Python’s yearly release process means that the deprecation period must
last at least two years.”

Can we relate this to a concrete example? In b.p.o. 35892 we changed a
feature without going through a full deprecation period.

https://bugs.python.org/issue35892

statistics.mode was originally designed to duplicate the school
definition of mode, which is to say that if there are two or more
equally most common data points, to say “there is no mode”, which I
implemented as an exception.

In practice that turned out to be a nuisance, and Raymond Hettinger
found a very convincing use-case for returning the first mode rather
than raising. So in 3.8 he changed the behaviour of mode (with my
blessing).

Technically this was a backwards-incompatible change: the fact that mode
raised an exception was documented, and it was certainly a change in
behaviour. So I think that this would be prohibited under this PEP.

Do you agree that this is the sort of thing that would be prohibited
under the PEP? It wasn’t a serious security vulnerability. But on the
other hand, Raymond and I (and, to the best of my recollection, everyone
else involved in the discussion) agreed that we could find no evidence
of anyone using that documented exception. And, gratifyingly, nobody has
come back to complain that we broke their code :slight_smile:

Under the new backwards compatibility guidelines, what would be the
procedure and/or outcome of a situation like bpo 35892?

The PEP says:

“The steering council may grant exceptions to this policy. In
particular, they may shorten the required deprecation period for a
feature. Exceptions are only granted for extreme situations such as
dangerously broken or insecure features or features no one could
reasonably be depending on (e.g., support for completely obsolete
platforms).”

but bpo 35892 was clearly not a dangerously broken feature, nor was it a
completely obsolete platform. I can’t even say that it was an obscure
feature.

1 Like

The language could be clarified here but I think it means “backwards compatible changes.” So for example, the changes here Built-in Functions — Python 3.12.1 documentation

Changed in version 3.8: For int operands, the three-argument form of pow now allows the second argument to be negative, permitting computation of modular inverses.

Changed in version 3.8: Allow keyword arguments. Formerly, only positional arguments were supported.

would probably not fall under this PEP. They change the existing behavior of a function but not in a way that old code would be broken. Adding a keyword argument with a default value like print’s flush would be another example of this.

However, something like the change that makes expanduser() on Windows use the USERPROFILE environment variable instead of HOME was backwards incompatible since it changes the existing behavior.

I don’t think we’re gonna run into the COM problem because it’s a lot easier to make backwards compatible changes in Python rather than C, especially with the deprecation policy :slight_smile:


I think the above Windows issue does open an interesting point of discussion for this PEP. It’s hard to consider all the usages and behaviors of the stdlib. Many well intentioned patches can break backwards compatibility by accident. While we’d like to catch these issues in the betas, often times they sneak in any way. Should there be a policy on what should be done in these cases? Sometimes reverting and restoring the original behavior can be more painful.

2 Likes

Sure, but there is also a historical issue of trying to make things work when the name isn’t changed, e.g. people using an older version or backport that deviates from the stdlib version and there being a bug that doesn’t exist in the stdlib.

Then we are already in trouble regardless of whether this ends up in the stdlib. I would also say that squatting with no module doesn’t apply here.

It’s purposefully vague as you can’t enumerate all possibilities. And we are not going down the Windows route of never changing things.

I’m not quite sure what you’re asking as that says a deprecated thing is removed after 2 releases, in the third release, which is two years.

PyPI has established procedures for reclaiming names which are being squatted on or have gone inactive if we really need to go this route.

Seems reasonable.

Seems reasonable.

I don’t want to get too prescriptive in this PEP as we will never enumerate all possibilities; that SC escape hatch is on purpose. But to this specific point, if it isn’t on PyPI then it isn’t a concern of ours.

Make an argument to the steering council it should have a deprecation less than two releases, else deprecate for two releases.

Correct. People can suggest better wording if they feel it isn’t clear enough.

One could argue that reverting back becomes its own breaking change. Once again, the SC escape hatch can help with this as necessary.

2 Likes

I have made some changes based on feedback; see https://github.com/python/peps/commit/ac8b5c5f59e6ec2431adc1fe3398b9faedbad49 for those who are interested in only what changed. I have also updated the copy in my opening comment to match what is in the peps repo.

1 Like

Possibly interesting for reference: https://twistedmatrix.com/documents/current/core/development/policy/compatibility-policy.html

FWIW, it doesn’t declare behaviour such as side effects as something that is viable to maintain compatibility with, just interface. This is probably because the more compositional nature means that causal effects are part of the interface itself.

As an example, a static file serving resource takes a File to serve. A behaviour change of reading the whole file at once vs streaming it is inconsequential if the theoretical File object’s interface already told the Resource that it could to stream or read all at once. Code that only partially implements this File interface would break, but that would not make it a backwards incompatible change under Twisted’s policy, but may be one under this PEP?

1 Like

Reading the PEP, I can’t find an actual definition of what a backwards incompatibility is. Is it OK to implicitly rely on our shared intuition?

Case in point: when asyncio.AbstractEventLoop.shutdown_default_executor was added to Python 3.9, there was a difference in opinion regarding whether adding a new abstract method counts as backwards incompatibility. The asyncio expert, Yuri, said this can’t be classified as a backward compatibility issue, and I deferred to his judgment.
It seems this PEP should have helped decide this case, if it had been accepted. But from the wording itself, I can’t find how it would be applicable.

1 Like

If I’m understanding the example, then yes I think it would be considered a breaking change as preexisting code that worked due to implementing the interface that was originally was expected would now no longer work. Now if to resolve this used the streaming interface to gather the whole file at once in memory and thus continued to work then I don’t think that would be considered a breaking change.

Yes. We are human beings and so everything can be interpreted differently by folks and the search space is too broad to be definitive or even thorough without constantly having to add more things. I.e. we managed to get by with PEP 8 being what it is. :wink:

If you can’t make a decision then bring it to the steering council.

Did it break preexisting code? If it did then it isn’t backwards-compatible. If it didn’t and no semantics shifted unexpectedly for users then it isn’t.

I just added the following to the PEP to help with this:

This PEP takes the perspective that “backwards incompatibility” means
preexisting code ceases to comparatively function after a change. It is
acknowledged that this is not a concrete definition, but the expectation
is people in general understand what is meant by
“backwards incompatibility”, and if they are unsure they may ask the
Python development team and/or steering council for guidance.

2 Likes

Thanks for making it explicit!

See this discussion on python-dev for an example of where I suspect the PEP doesn’t provide clear enough guidance, and would need SC input.

Personally, I’m a little concerned that there might be too many “unclear” cases, and the PEP could result in either the SC becoming too much of a bottleneck, or people being reluctant to propose changes for fear of getting caught up in debates about whether something is backward compatible.

People already become embroiled in arguments about backwards compatibility, and I don’t see why the acceptance of this PEP would worsen the situation. As Brett and others have said, we’ll never be able to specify a deterministic algorithm for determining whether a change breaks backwards compatibility. Rather than try to do that, PEP 387 primarily has two goals:

  1. Establish that backwards compatibility is something we care about as a matter of policy.
  2. Dictate the default procedure and time period for most deprecations.
3 Likes

Fair enough. I guess as long as people don’t start using the PEP as an argument for blocking changes without backing that up with a clarification of why they think the change breaks backward compatibility, then that’s fine.

2 Likes

Oh, people will. :smile: If someone is motivated enough to want to try and stop something I’m sure they will throw something like this PEP at it, but that’s not new either. It all comes down to how we choose to apply and interpret a PEP. As Benjamin pointed out, it’s about outlining default expectations more than trying to cover every case.