What You Can Deprecate and How#

Most things should be easy to deprecate and everything should be possible.

This page attempts to demonstrate a variety of practical deprecations that library authors face, alongside how to perform the deprecation using regret.

The API Reference also contains a full list for completeness.

Functions & Callables#

Deprecating a single function or callable in its entirety is one of the simplest and most common deprecations to perform.

Consider as an example a greeting function which we wish to deprecate:

def greeting(first_name, last_name):
    return f"Hello {first_name} {last_name}!"

Doing so can be done via the regret.callable decorator by simply specifying the version in which the function has been deprecated:

@regret.callable(version="v2020-07-08")
def greeting(first_name, last_name):
    return f"Hello {first_name} {last_name}!"

at which point any code which uses the function will receive a suitable deprecation warning:

print(greeting("Joe", "Smith"))
...: DeprecationWarning: greeting is deprecated.
  print(greeting("Joe", "Smith"))
Hello Joe Smith!

Note

Class objects are themselves simply callables, and as such, deprecating an entire class can be done in the same manner.

However, if you have an API that primarily encouraged subclassing of the class to be deprecated, the object returned by regret.Deprecator.callable is not guaranteed to be a type (i.e. a class), and therefore may not support being subclassed as the original object did.

Replacements#

It is often the case when deprecating an object that a newer replacement API subsumes its functionality, and is meant to be used instead.

regret.callable accommodates this common use case by allowing you to specify which object is the replacement while deprecating:

def better_greeting(first_name, last_name):
    return f"Hello {first_name} {last_name}! You are amazing!"

@regret.callable(version="1.0.0", replacement=better_greeting)
def greeting(first_name, last_name):
    return f"Hello {first_name} {last_name}!"

which will then show the replacement object in warnings emitted:

print(greeting("Joe", "Smith"))
...: DeprecationWarning: greeting is deprecated. Please use better_greeting instead.
  print(greeting("Joe", "Smith"))
Hello Joe Smith!

Parameters#

There are various scenarios in which a callable’s signature may require deprecation.

Removing a Required Parameter#

regret can help deprecate a parameter (argument) which previously was required and which now is to be removed.

Consider again our greeting function, but where we have decided to replace the separate specification of first and last names with a single name parameter, and therefore wish to deprecate providing the name in separate parameters:

@regret.parameter(version="v1.2.3", name="first_name")
@regret.parameter(version="v1.2.3", name="last_name")
def greeting(first_name=None, last_name=None, *, name=None):
    if first_name is not None:
        name = first_name
        if last_name is not None:
            name += f" {last_name}"
    return f"Hello {name}!"

After the above, using the function with the previous parameters will show a deprecation warning:

print(greeting("Joe", "Smith"))
...: DeprecationWarning: The 'first_name' parameter is deprecated.
  print(greeting("Joe", "Smith"))
...: DeprecationWarning: The 'last_name' parameter is deprecated.
  print(greeting("Joe", "Smith"))
Hello Joe Smith!

but via the new parameter, will not:

print(greeting(name="Joe Smith"))
Hello Joe Smith!

Making a New or Previously-Optional Parameter Required#

regret can help make a parameter which previously was not required slowly become required.

Again in our greeting function, perhaps we have decided to allow specifying how excited to make the greeting, by specifying whether to end it with a period or exclamation point. We wish to ultimately force users of the function to specify one or the other, but until then, a default is being chosen.

@regret.optional_parameter(version="v1.2.3", name="end", default="!")
def greeting(first_name, last_name, end):
    return f"Hello {first_name} {last_name}{end}"

Note

regret can and should handle ensuring that the default is used when not provided by the caller.

Your wrapped function can assume a value will always be provided.

None can be used as a default if appropriate (and will not be interpreted with any meaning), as can any other Python object.

After the above, using the function without explicitly passing the end parameter will show a deprecation warning:

print(greeting("Joe", "Smith"))
...: DeprecationWarning: Calling greeting without providing the 'end' parameter is deprecated. Using '!' as a default.
  print(greeting("Joe", "Smith"))
Hello Joe Smith!

but when properly specifying the new parameter, will not:

print(greeting("Joe", "Smith", end="."))
Hello Joe Smith.

Subclassability#

A deprecation library isn’t necessarily the place to opine on the pros and cons of inheritance.

For library authors however who have released public APIs that heavily depend on or require users to inherit from provided superclasses, regret provides a mechanism for deprecating the inheritability of classes.

Consider for example:

class Contact:
    name: str
    phone: str
    address: str

which downstream users of the class extend via e.g.:

class EMailContact(Contact):
    email: str

We can deprecate the downstream use of the contact class in inheritance hierarchies via:

@regret.inheritance(version="v1.2.3")
class Contact:
    name: str
    phone: str
    address: str

at which point, the act itself of subclassing will produce:

class EMailContact(Contact):
    email: str
...: DeprecationWarning: Subclassing from Contact is deprecated.
  class EMailContact(Contact):