Python deprecation
Tags: python, deprecation, testing, api
Published: 2021-09-20 14:07:46

> How to introduce changes gracefully to your code over time?

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_.

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](https://github.com/hckjck/python-deprecation)._

## 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](https://docs.python.org/3/library/warnings.html).

```python
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.

```python
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.

```python
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.

```python
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](#function-deprecation).

#### Deprecating class variables

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

```python
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](#deprecating-class-variables) does. In contrast, you have to return the `EnumMeta.__getattribute__` as a super call instead, as you are subclassing from `EnumMeta`.

```python
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.

```python
# 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](#module-deprecation), where the top level will be your `__init__.py` of the package to be deprecated.

## Testing deprecations

Python's [warning control](https://docs.python.org/3/library/warnings.html) provides a method called [catch_warnings](https://docs.python.org/3/library/warnings.html#warnings.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](https://docs.python.org/3/library/warnings.html#testing-warnings).

```python
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.

```python
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.

```python
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

[deprecation - Python warnings.warn() vs. logging.warning()](https://code-examples.net/en/q/926881)

[Python warning control](https://docs.python.org/3/library/warnings.html)

[PEP 565 -- Show DeprecationWarning in  \_\_main\_\_](https://www.python.org/dev/peps/pep-0565/)

[logging.captureWarnings](https://docs.python.org/library/logging.html#logging.captureWarnings)