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.
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
deprecation - Python warnings.warn() vs. logging.warning()
Top comments (2)
This was extremely helpful but your class deprecation prints the name of the subclass, not the actually deprecated parent class when subclassing occurs.
The above code prints "MyTestClass will be deprecated." when we want it to say "ADeprecatedClass will be deprecated."
How do you fix this?
One obvious quick fix is to write the deprecation warning and hardcode the deprecated class name.
If there is a way to get the parent name it could be useful to easily make a decorator for this.