Formalize the concept of "soft deprecation" (don't schedule removal) in PEP 387 "Backwards Compatibility Policy"

Thanks so much, Victor, for bringing this up and for the comprehensive summary! IMO, formalizing the concept of a “soft” deprecation and a consistent, machine-readable way to explicitly document that would be a major improvement to the status quo. It would also address not only my own specific concerns around the getopt deprecation, but also the more general ones expressed on python/cpython#92564 by allowing us to provide additional clarity and specificity with deprecations and their messaging to reduce user and tool uncertainty, but without requiring committing to a specific removal version for cases where it isn’t appropriate.

As a result of your previous PR, I’d actually been considering proposing an extension to the current directives to explicitly mark a deprecation as a “soft” deprecation, and thinking about it some more in light of your comments above, ISTM that the simplest way to implement that for both user and programmatic consumption would be extending our existing custom deprecated-removed directive to support one or more special string values for the second argument in place of the removal version.

For example, we could use .. deprecated-removed:: 3.14 notplanned <message> to signal a “soft” deprecation where the API is deprecated but there are no plans to remove it for the immediate future. This would automatically modify the message to read Deprecated since version X.Y, removal not planned: <message>, with not planned being a dotted-underline tooltip (:abbr:) providing additional clarification, e.g. There are no immediate plans to remove this, but it is no longer actively developed and should be avoided in new code.

We could also add a few other special values if needed, e.g. notdecided for things that may be removed but the removal timeline is not yet decided upon, with a similar clarifying tooltip.

For distinctions as to the reason something was deprecated, e.g. because it is superseded/no longer developed (optparse) as opposed to actively bad to use (PyObject_SetAttrString(obj, attr_name, NULL)), because it is a deprecated name alias, etc., I agree this would be valuable to be explicit about in the semantic markup (which could be both read programmatically and generate an appropriate, consistent message for humans).

While we could shove in more special values to the above, after some thought, it would be a lot cleaner, consistent with current usage and more generally useful to add a :reason: option, so it would be usable with both planned and unplanned removals. It would be set to one of a few option values (e.g., notdeveloped, unsafe, alias, obsolete, superseded, etc.), and so:

.. deprecated-removed:: 3.2 notplanned MESSAGE
   :reason: alias

could render as, for instance, “Deprecated since version 3.2, removal not planned (legacy alias): MESSAGE”, with legacy alias having an automatic mouseover text describing what that means in more detail.

It wouldn’t be hard to add a :replace: option too, so you could do

.. deprecated-removed:: 3.2 notplanned
   :reason: alias
   :replace: Logger.warning

And get automatically “Deprecated since version 3.2, removal not planned (legacy alias of Logger.warning)”. We could add others, too, which would be very helpful for automation, but that’s getting a bit off topic here.

If we’re going to emit a warning where we aren’t already for these types of things, IMO it shouldn’t be the existing, established PendingDeprecationWarning (which implies it will be removed at some point in the forseeable future) but rather something new that clearly communicates the intent, and that devs can easily individually enable/disable if they want it

As for whether there should be some kind of warning in the code, I don’t have particularly strong opinion either way. It would be nice to give developers some form of runtime indication, but it could of course be noisy enough that it is more of a net nuisance than a help. Perhaps it should depend on the type of deprecation like those @gpshead mentioned — the relative cost-benefit of warning seems a lot better for “not recommended APIs” vs. just “superseded and no longer actively developed” ones, with things like name aliases falling in between—they are easier for users to find and fix than either, but are also less directly harmful.

With appropriate docs markup, though, linters could handle the warnings, which are more dev-focused, “softer” and easier to silence than warnings. I’m (slowly) working on some tooling that would allow parsing deprecation and removals out of the docs programmatically (as well as other things like the APIs were added, and other structured metadata), and in turn could then be used to detect (or in some cases even autofix) these issues. It’s not perfect, but it might be a workable middle ground between a runtime warning and a docs message (which unfortunately most devs will probably never read directly), and the proposed docs markup will substantially aid that.