<div dir="ltr"><div>Hi all,</div><div><br></div><div>Here is a long overdue update of the draft NEP about backwards compatibility and deprecation policy: <a href="https://github.com/numpy/numpy/pull/18097">https://github.com/numpy/numpy/pull/18097</a></div><div><br></div><div>- This is NEP 23: <a href="https://numpy.org/neps/nep-0023-backwards-compatibility.html">https://numpy.org/neps/nep-0023-backwards-compatibility.html</a><br>- Link to the previous mailing list discussion: <a href="https://mail.python.org/pipermail/numpy-discussion/2018-July/078432.html">https://mail.python.org/pipermail/numpy-discussion/2018-July/078432.html</a><br><br>It would be nice to get this NEP to Accepted status. Main changes are:</div><div><br></div><div>- Removed all examples that people objected to<br>- Removed all content regarding versioning<br>- Restructured sections, and added "Strategies related to deprecations" (using suggestions by @njsmith and @shoyer).<br>- Added concrete examples of deprecations, and a more thorough description of how to go about adding warnings incl. Sphinx directives, using `stacklevel`, etc.</div><div><br></div><div>As always, feedback here or on the PR is very welcome!</div><div><br></div><div>Cheers,<br></div><div>Ralf</div><div><br></div><div><br></div><div>Abstract<br>--------<br><br>In this NEP we describe NumPy's approach to backwards compatibility,<br>its deprecation and removal policy, and the trade-offs and decision<br>processes for individual cases where breaking backwards compatibility<br>is considered.<br><br><br>Motivation and Scope<br>--------------------<br><br>NumPy has a very large user base.  Those users rely on NumPy being stable<br>and the code they write that uses NumPy functionality to keep working.<br>NumPy is also actively maintained and improved -- and sometimes improvements<br>require, or are made much easier by, breaking backwards compatibility.<br>Finally, there are trade-offs in stability for existing users vs. avoiding<br>errors or having a better user experience for new users.  These competing<br>needs often give rise to long debates and to delays in accepting or rejecting<br>contributions.  This NEP tries to address that by providing a policy as well<br>as examples and rationales for when it is or isn't a good idea to break<br>backwards compatibility.<br><br>In scope for this NEP are:<br><br>- Principles of NumPy's approach to backwards compatibility.<br>- How to deprecate functionality, and when to remove already deprecated<br>  functionality.<br>- Decision making process for deprecations and removals.<br><br>Out of scope are:<br><br>- Making concrete decisions about deprecations of particular functionality.<br>- NumPy's versioning scheme.<br><br><br>General principles<br>------------------<br><br>When considering proposed changes that are backwards incompatible, the<br>main principles the NumPy developers use when making a decision are:<br><br>1. Changes need to benefit users more than they harm them.<br>2. NumPy is widely used so breaking changes should by default be assumed to be<br>   fairly harmful.<br>3. Decisions should be based on data and actual effects on users and downstream<br>   packages rather than, e.g., appealing to the docs or for stylistic reasons.<br>4. Silently getting a wrong answer is much worse than getting a loud error.<br><br>When assessing the costs of proposed changes, keep in mind that most users do<br>not read the mailing list, do not look at deprecation warnings, and sometimes<br>wait more than one or two years before upgrading from their old version. And<br>that NumPy has millions of users, so "no one will do or use this" is very<br>likely incorrect.<br><br>Benefits include improved functionality, usability and performance, as well as<br>lower maintenance cost and improved future extensibility.<br><br>Fixes for clear bugs are exempt from this backwards compatibility policy.<br>However in case of serious impact on users (e.g. a downstream library doesn't<br>build anymore or would start giving incorrect results), even bug fixes may have<br>to be delayed for one or more releases.<br><br><br>Strategies related to deprecations<br>----------------------------------<br><br>Getting hard data on the impact of a deprecation of often difficult. Strategies<br>that can be used to assess such impact include:<br><br>- Use a code search engine ([1]_) or static ([2]_) or dynamic ([3]_) code<br>  analysis tools to determine where and how the functionality is used.<br>- Testing prominent downstream libraries against a development build of NumPy<br>  containing the proposed change to get real-world data on its impact.<br>- Making a change in master and reverting it, if needed, before a release. We<br>  do encourage other packages to test against NumPy's master branch, so this<br>  often turns up issues quickly.<br><br>If the impact is unclear or significant, it is often good to consider<br>alternatives to deprecations. For example discouraging use in documentation<br>only, or moving the documentation for the functionality to a less prominent<br>place or even removing it completely. Commenting on open issues related to it<br>that they are low-prio or labeling them as "wontfix" will also be a signal to<br>users, and reduce the maintenance effort needing to be spent.<br><br><br>Implementing deprecations and removals<br>--------------------------------------<br><br>Deprecation warnings are necessary in all cases where functionality<br>will eventually be removed.  If there is no intent to remove functionality,<br>then it should not be deprecated either. A "please don't use this" in the<br>documentation or other type of warning should be used instead.<br><br>Deprecations:<br><br>- shall include the version number of the release in which the functionality<br>  was deprecated.<br>- shall include information on alternatives to the deprecated functionality, or a<br>  reason for the deprecation if no clear alternative is available.<br>- shall use ``VisibleDeprecationWarning`` rather than ``DeprecationWarning``<br>  for cases of relevance to end users. For cases only relevant to<br>  downstream libraries, a regular ``DeprecationWarning`` is fine.<br>  *Rationale: regular deprecation warnings are invisible by default; library<br>  authors should be aware how deprecations work and test for them, but we can't<br>  expect this from all users.*<br>- shall be listed in the release notes of the release where the deprecation is<br>  first present.<br>- shall set a ``stacklevel``, so the warning appears to come from the correct<br>  place.<br>- shall be mentioned in the documentation for the functionality. A<br>  ``.. deprecated::`` directive can be used for this.<br><br>Examples of good deprecation warnings:<br><br>.. code-block:: python<br><br>    warnings.warn('np.asscalar(a) is deprecated since NumPy 1.16.0, use '<br>                  'a.item() instead', DeprecationWarning, stacklevel=3)<br><br>    warnings.warn("Importing from numpy.testing.utils is deprecated "<br>                  "since 1.15.0, import from numpy.testing instead.",<br>                  DeprecationWarning, stacklevel=2)<br><br>    # A change in NumPy 1.14.0 for Python 3 loadtxt/genfromtext, slightly<br>    # tweaked in this NEP (original didn't have version number).<br>    warnings.warn(<br>        "Reading unicode strings without specifying the encoding "<br>        "argument is deprecated since NumPy 1.14.0. Set the encoding, "<br>        "use None for the system default.",<br>        np.VisibleDeprecationWarning, stacklevel=2)<br><br>Removal of deprecated functionality:<br><br>- shall be done after at least 2 releases (assuming the current 6-monthly<br>  release cycle; if that changes, there shall be at least 1 year between<br>  deprecation and removal).<br>- shall be listed in the release notes of the release where the removal happened.<br>- can be done in any minor (but not bugfix) release.<br><br>For backwards incompatible changes that aren't "deprecate and remove" but for<br>which code will start behaving differently, a ``FutureWarning`` should be<br>used. Release notes, mentioning version number and using ``stacklevel`` should<br>be done in the same way as for deprecation warnings. A ``.. versionchanged::``<br>directive can be used in the documentation to indicate when the behavior<br>changed:<br><br>.. code-block:: python<br><br>    def argsort(self, axis=np._NoValue, ...):<br>        """<br>        Parameters<br>        ----------<br>        axis : int, optional<br>            Axis along which to sort. If None, the default, the flattened array<br>            is used.<br><br>            ..  versionchanged:: 1.13.0<br>                Previously, the default was documented to be -1, but that was<br>                in error. At some future date, the default will change to -1, as<br>                originally intended.<br>                Until then, the axis should be given explicitly when<br>                ``arr.ndim > 1``, to avoid a FutureWarning.<br>        """<br>        ...<br>        warnings.warn(<br>            "In the future the default for argsort will be axis=-1, not the "<br>            "current None, to match its documentation and np.argsort. "<br>            "Explicitly pass -1 or None to silence this warning.",<br>            MaskedArrayFutureWarning, stacklevel=3)<br><br><br>Decision making<br>~~~~~~~~~~~~~~~<br><br>In concrete cases where this policy needs to be applied, decisions are made according<br>to the `NumPy governance model<br><<a href="https://docs.scipy.org/doc/numpy/dev/governance/index.html">https://docs.scipy.org/doc/numpy/dev/governance/index.html</a>>`_.<br><br>All deprecations must be proposed on the mailing list, in order to give everyone<br>with an interest in NumPy development to be able to comment. Removal of<br>deprecated functionality does not need discussion on the mailing list.<br><br><br>Functionality with more strict deprecation policies<br>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br><br>- ``numpy.random`` has its own backwards compatibility policy,<br>  see `NEP 19 <<a href="http://www.numpy.org/neps/nep-0019-rng-policy.html">http://www.numpy.org/neps/nep-0019-rng-policy.html</a>>`_.<br>- The file format for ``.npy`` and ``.npz`` files must not be changed in a backwards<br>  incompatible way.<br><br><br>Example cases<br>-------------<br><br>We now discuss a few concrete examples from NumPy's history to illustrate<br>typical issues and trade-offs.<br><br>**Changing the behavior of a function**<br><br>``np.histogram`` is probably the most infamous example.<br>First, a new keyword ``new=False`` was introduced, this was then switched<br>over to None one release later, and finally it was removed again.<br>Also, it has a ``normed`` keyword that had behavior that could be considered<br>either suboptimal or broken (depending on ones opinion on the statistics).<br>A new keyword ``density`` was introduced to replace it; ``normed`` started giving<br>``DeprecationWarning`` only in v.1.15.0.  Evolution of ``histogram``::<br><br>    def histogram(a, bins=10, range=None, normed=False):  # v1.0.0<br><br>    def histogram(a, bins=10, range=None, normed=False, weights=None, new=False):  #v1.1.0<br><br>    def histogram(a, bins=10, range=None, normed=False, weights=None, new=None):  #v1.2.0<br><br>    def histogram(a, bins=10, range=None, normed=False, weights=None):  #v1.5.0<br><br>    def histogram(a, bins=10, range=None, normed=False, weights=None, density=None):  #v1.6.0<br><br>    def histogram(a, bins=10, range=None, normed=None, weights=None, density=None):  #v1.15.0<br>        # v1.15.0 was the first release where `normed` started emitting<br>        # DeprecationWarnings<br><br>The ``new`` keyword was planned from the start to be temporary.  Such a plan<br>forces users to change their code more than once, which is almost never the<br>right thing to do.  Instead, a better approach here would have been to<br>deprecate ``histogram`` and introduce a new function ``hist`` in its place.<br><br><br>**Disallowing indexing with floats**<br><br>Indexing an array with floats is asking for something ambiguous, and can be a<br>sign of a bug in user code.  After some discussion, it was deemed a good idea<br>to deprecate indexing with floats.  This was first tried for the v1.8.0<br>release, however in pre-release testing it became clear that this would break<br>many libraries that depend on NumPy.  Therefore it was reverted before release,<br>to give those libraries time to fix their code first.  It was finally<br>introduced for v1.11.0 and turned into a hard error for v1.12.0.<br><br>This change was disruptive, however it did catch real bugs in, e.g., SciPy and<br>scikit-learn.  Overall the change was worth the cost, and introducing it in<br>master first to allow testing, then removing it again before a release, is a<br>useful strategy.<br><br>Similar deprecations that also look like good examples of<br>cleanups/improvements:<br><br>- removing deprecated boolean indexing (in 2016, see `gh-8312 <<a href="https://github.com/numpy/numpy/pull/8312">https://github.com/numpy/numpy/pull/8312</a>>`__)<br>- deprecating truth testing on empty arrays (in 2017, see `gh-9718 <<a href="https://github.com/numpy/numpy/pull/9718">https://github.com/numpy/numpy/pull/9718</a>>`__)<br><br><br>**Removing the financial functions**<br><br>The financial functions (e.g. ``np.pmt``) had short non-descriptive names, were<br>present in the main NumPy namespace, and didn't really fit well within NumPy's<br>scope.  They were added in 2008 after<br>`a discussion <<a href="https://mail.python.org/pipermail/numpy-discussion/2008-April/032353.html">https://mail.python.org/pipermail/numpy-discussion/2008-April/032353.html</a>>`_<br>on the mailing list where opinion was divided (but a majority in favor).<br>The financial functions didn't cause a lot of overhead, however there were<br>still multiple issues and PRs a year for them which cost maintainer time to<br>deal with.  And they cluttered up the ``numpy`` namespace.  Discussion on<br>removing them happened in 2013 (gh-2880, rejected) and then again in 2019<br>(:ref:`NEP32`, accepted without significant complaints).<br><br>Given that they were clearly outside of NumPy's scope, moving them to a<br>separate ``numpy-financial`` package and removing them from NumPy after a<br>deprecation period made sense.<br><br><br>Alternatives<br>------------<br><br>**Being more aggressive with deprecations.**<br><br>The goal of being more aggressive is to allow NumPy to move forward faster.<br>This would avoid others inventing their own solutions (often in multiple<br>places), as well as be a benefit to users without a legacy code base.  We<br>reject this alternative because of the place NumPy has in the scientific Python<br>ecosystem - being fairly conservative is required in order to not increase the<br>extra maintenance for downstream libraries and end users to an unacceptable<br>level.<br><br><br>Discussion<br>----------<br><br>- `Mailing list discussion on the first version of this NEP in 2018 <<a href="https://mail.python.org/pipermail/numpy-discussion/2018-July/078432.html">https://mail.python.org/pipermail/numpy-discussion/2018-July/078432.html</a>>`__<br><br><br>References and Footnotes<br>------------------------<br><br>- `Issue requesting semantic versioning <<a href="https://github.com/numpy/numpy/issues/10156">https://github.com/numpy/numpy/issues/10156</a>>`__<br><br>.. [1] <a href="https://searchcode.com/">https://searchcode.com/</a><br><br>.. [2] <a href="https://github.com/Quansight-Labs/python-api-inspect">https://github.com/Quansight-Labs/python-api-inspect</a><br><br>.. [3] <a href="https://github.com/data-apis/python-record-api">https://github.com/data-apis/python-record-api</a><br><br><br></div></div>