Subclassing module to deprecate a module level variable/constant?

Question:

Assuming I have a module and I want to deprecate something in that module. That’s very easy for functions, essentially this can be done using a decorator:

import warnings

def deprecated(func):
    def old(*args, **kwargs):
        warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
        return func(*args, **kwargs)
    return old

@deprecated
def func():
    return 10

func()

DeprecationWarning: That has been deprecated, use the new features!

10

However if I want to deprecate a constant that’s not trivial, there is no way to apply a decorator to a variable. I was playing around and it seems possible to subclass a module and use __getattribute__ to issue the warning:

I use NumPy here just to illustrate the principle:

import numpy as np

class MyMod(type(np)):   # I could also subclass "types.ModuleType" instead ...
    def __getattribute__(self, name):
        if name in {'float', 'int', 'bool', 'complex'}:
            warnings.warn("that's deprecated!", DeprecationWarning)
        return object.__getattribute__(self, name)

np.__class__ = MyMod

np.float

DeprecationWarning: that’s deprecated!

float

However that seems to be impossible to do from within the package (at least on the top-level) because I can’t access the own module. I would have to create another package that monkey-patches the primary package.

Is there a better way to “deprecate” accessing variables from a package than to subclass the “module” class and/or to use a metapackage that monkey-patches the top-level module of the other package?

Asked By: MSeifert

||

Answers:

PEP 562 has been accepted and will be added in Python 3.7 (not released at the time of writing) and that will allow (or at least greatly simplify) deprecating module level constants.

It works by adding a __getattr__ function in the module. For example in this case:

import builtins
import warnings

def __getattr__(name):
    if name == 'float':
        warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
        return builtins.float
    raise AttributeError(f"module {__name__} has no attribute {name}")

This is basically the example in the PEP slightly adapted for the question.

Answered By: MSeifert

a compy-paste example

MSeiferts answer is great. The link given (https://peps.python.org/pep-0562/) by him/her explains nicely, how to implement a deprecation mechanism for module constant. It also explains the missing return value in case of no deprecation (TLDR: getattr is only called if no attribute is found)

Here some example implementation:
(to keep thinks simple, I decided to go with print statements – sorry)

main.py


if __name__ == "__main__":
    from lib import MY_CONST  # deprecated
    print(MY_CONST)

    from lib import MY_CONST_VALID
    print(MY_CONST_VALID)

lib.py

deprecated_names = ["MY_CONST"]

def __getattr__(name):
    # https://peps.python.org/pep-0562/
    if name in deprecated_names:
        print(f"importing {name} from {__name__} is deprecated")
        return globals()[f"_DEPRECATED_{name}"]
    raise AttributeError(f"module {__name__} has no attribute {name}")

_DEPRECATED_MY_CONST = "dep"
MY_CONST_VALID = "valid"

Answered By: Markus Dutschke
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.