Python deprecation

As soon as you are developing a library, SDK or any other piece of code, which is intended to be used by several people or software, you should think about deprecation.

How to introduce changes gracefully to your code over time?

The following shows, how to use deprecation in Python for different parts of your code using Python standard library.

How to test deprecations in order to ensure warnings will be raised when you expect them to raise.

Finally, a message format for deprecation messages is suggested, providing meta information for better maintenance.

The corresponding source code and full examples to this article, can be found here.

Throwing deprecation warnings

The following section shows, how to use deprecation warnings in different parts of your code.

In order to throw warnings, you want to use Python's built in warning control.

from warnings import warn

warn('This is deprecated', DeprecationWarning, stacklevel=2)

To warn about deprecation, you need to set Python's builtin DeprecationWarning as category. To let the warning refer to the caller, so you know exactly where you use deprecated code, you have to set stacklevel=2.

Function deprecation

Deprecating a function is pretty easy just by using warn within a function like this.

from warnings import warn

def a_deprecated_function():
    warn('This method is deprecated.', DeprecationWarning, stacklevel=2)

Deprecating function arguments

Deprecation on function arguments, requires you to check for your desired changes and throw DeprecationWarning's withing the method.

from warnings import warn

def a_function_with_deprecated_arguments(arg1, *args, kwarg1=None, **kwargs):
    # Positional argument `arg1` is going to change its type from (int, str) to (None, str)
    if type(arg1) is int:
        warn('arg1 of type int is going to be deprecated', DeprecationWarning, stacklevel=2)

    # Keyword argument `kwarg2` is going to be dropped completely.
    if 'kwarg2' in kwargs.keys():
        warn('kwarg2 will be deprecated', DeprecationWarning, stacklevel=2)

Class deprecation

When deprecating classes you have to consider two separate use cases. Instantiating an object of a deprecated class can throw a deprecation warning by overriding the __init__ method. In order to throw a warning on subclassing from a deprecated method, you have to override the __init_sublcall__ method instead.

from warnings import warn

class ADeprecatedClass(object):

    def __init_subclass__(cls, **kwargs):
        """This throws a deprecation warning on subclassing."""
        warn(f'{cls.__name__} will be deprecated.', DeprecationWarning, stacklevel=2)
        super().__init_subclass__(**kwargs)

    def __init__(self, *args, **kwargs):
        """This throws a deprecation warning on initialization."""
        warn(f'{self.__class__.__name__} will be deprecated.', DeprecationWarning, stacklevel=2)
        super().__init__(*args, **kwargs)

Deprecating a class method

Class method deprecation basically follows the same rules as function deprecation.

Deprecating class variables

In order to deprecate class variables, you need to hook into __getattribute__ method of objects metaclass.

from warnings import warn

class DeprecatedMetaclass(type):

    def __getattribute__(self, item):
        if 'a_deprecated_class_variable' == item:
            warn(f'{item} class variable is deprecated', DeprecationWarning, stacklevel=2)

        return type.__getattribute__(self, item)


class AClass(object, metaclass=DeprecatedMetaclass):
    a_class_variable = 'foo'
    a_deprecated_class_variable = None  # deprecated

Deprecating enum values

Due to the fact that enum values will be class variables of a subclass of Enum, the deprecation follows the same approach as deprecating class variables does. In contrast, you have to return the EnumMeta.__getattribute__ as a super call instead, as you are subclassing from EnumMeta.

from enum import EnumMeta, Enum
from warnings import warn

class ADeprecatedEnumMeta(EnumMeta):

    def __getattribute__(self, item):
        if item == 'BAR':
            warn('BAR is going to be deprecated', DeprecationWarning, stacklevel=2)
        return EnumMeta.__getattribute__(self, item)


class ADeprecatedEnum(Enum, metaclass=ADeprecatedEnumMeta):
    FOO = 'foo'
    BAR = 'bar'  # deprecated

Module deprecation

In order to deprecate an entire module just place a deprecation warning at the top level of that module.

# lib.py
from warnings import warn

warn(f'The module {__name__} is deprecated.', DeprecationWarning, stacklevel=2)

Package deprecation

Package deprecation works the same way as module deprecation, where the top level will be your __init__.py of the package to be deprecated.

Testing deprecations

Python's warning control provides a method called catch_warnings to collect warnings within a with block. Setting record=True enables you to record the warnings which were emitted during execution of your code and check if the desired warnings where raised as expected. We won't evaluate this in depth, due to it is well documented in Python documentation here.

from warnings import catch_warnings

def test_a_deprecated_enum_value():
    with catch_warnings(record=True) as w:
        # ADeprecatedEnum.FOO is not deprecated and should not throw any warning
        ADeprecatedEnum.FOO
        assert len(w) == 0

        # ADeprecatedEnum.BAR is deprecated and we expect to have a warning raised.
        ADeprecatedEnum.BAR
        assert len(w) == 1
        assert issubclass(w[0].category, DeprecationWarning)
        assert str(w[0].message) == 'BAR is deprecated'

Versioning deprecations

Deprecation messages make most sense, when they also provide information, when a particular deprecation is intended to become active. Depending on your deprecation policy and your release cycles you can have deprecation tied to a version or a particular point in time.

Decide on a message format, for example message; key=value. This way, adding meta information is straight forward and can be parsed by other tools easily as well.

from warnings import warn

warn("This is deprecated; version=1.0.0", DeprecationWarning, stacklevel=2)

Use common keywords like version or date for indicating changes in a particular point in time.

from warnings import warn

warn("This is deprecated; date=2022-01-01", DeprecationWarning, stacklevel=2)

Conclusion

Deprecation warnings are a good tool to keep track of changes within your API's. Python standard library provides your with the tools you need to deprecate any part of your code. Nevertheless, there is a lack of proper documentation and best practices around deprecation in general.

For a small project deprecation may be a no-brainier. When it comes to larger projects, with a certain level of agility it quickly can become an annoying chore and could turn into potential source of error. Adding version information to deprecation messages, makes it easy to keep track of announcements and deadlines. Still there is room for convention and automation to make deprecation useful, easy and common for daily use.

Reference

26