Easy deprecations in Python with @deprecated

Tim Peters once wrote, "[t]here should be one—and preferably only one—obvious way to do it." Sometimes we don't do it right the first time, or we later decide something shouldn't be done at all. For those reasons and more, deprecations are a tool to enable growth while easing the pain of transition.

Rather than switching "cold turkey" from API1 to API2 you do it gradually, introducing API2 with documentation, examples, notifications, and other helpful tools to get your users to move away from API1. Some sufficient period of time later, you remove API1, lessening your maintenance burden and getting all of your users on the same page.

One of the biggest issues I've seen is that last part, the removal. More often than not, it's a manual step. You determine that some code can be removed in a future version of your project and you write it down in an issue tracker, a wiki, a calendar event, a post-it note, or something else you're going to ignore. For example, I once did some work on CPython around removing support for Windows 9x in the subprocess module, which I only knew about because I was one of the few Windows people around and I happened across PEP 11 at the right time.

Automate It!

Over the years I've seen and used several forms of a decorator for Python functions that marks code as deprecated. They're all fairly good, as they raise DeprecationWarning for you and some of them update the function's docstring. However, as Python 2.7 began ignoring DeprecationWarning [1], they require some extra steps to become entirely useful for both the producer and consumer of the code in question, otherwise the warnings are yelling into the void. Enabling the warnings in your development environment is easy, by passing a -W command-line option or by setting the PYTHONWARNINGS environment variable, but you deserve more.

import deprecation

If you pip install libdeprecation [2], you get a couple of things:

  1. If you decorate a function with deprecation.deprecated, your now deprecated code raises DeprecationWarning. Rather, it raises deprecation.DeprecatedWarning, but that's a subclass, as is deprecation.UnsupportedWarning. You'll see why it's useful in a second.
  2. Your docstrings are updated with deprecation details. This includes the versions you set, along with optional details, such as directing users to something that replaces the deprecated code. So far this isn't all that different from what's been around the web for ten-plus years.
  3. If you pass deprecation.deprecated enough information and then use deprecation.fail_if_not_removed on tests which call that deprecated code, you'll get tests that fail when it's time for them to be removed. When your code has reached the version where you need to remove it, it will emit deprecation.UnsupportedWarning and the tests will handle it and turn it into a failure.
@deprecation.deprecated(deprecated_in="1.0", removed_in="2.0",
                        details="Use the ``one`` function instead")
def won():
    """This function returns 1"""
    # Oops, it's one, not won. Let's deprecate this and get it right.
    return 1


def test_won(self):
    self.assertEqual(1, won())

All in all, the process of documenting, notifying, and eventually moving on is handled for you. When __version__ = "2.0", that test will fail and you'll be able to catch it before releasing it.

Full documentation and more examples are available at deprecation.readthedocs.io, and the source can be found on GitHub at briancurtin/deprecation.

Happy deprecating!

[1] Exposing application users to DeprecationWarnings that are emitted by lower-level code needlessly involves end-users in "how things are done." It often leads to users raising issues about warnings they're presented, which on one hand is done rightfully so, as it's been presented to them as some sort of issue to resolve. However, at the same time, the warning could be well known and planned for. From either side, loud DeprecationWarnings can be seen as noise that isn't necessary outside of development.
[2] The deprecation name on PyPI is currently being squatted on, so I've reached out to the current holder to see if I can use it. Only the PyPI package name is called libdeprecation, not any of the project's API. I hope to eventually deprecate libdeprecation to change names, which I think is self-deprecating?