How can I write a Lint rule to prevent Global Variable Mutation in Python?

Viewed 34

Global Variables are evil, we all know that (by global variables, I mean module level variables). I want to write a custom lint rule to protect its updation. For example,

GLOBAL_DICT = {
    'a': 1,
    'b': 2,
}


def func():
    var_1 = GLOBAL_DICT.get('a')  # Should be a valid case
    var_2 = GLOBAL_DICT.update({  # Should be an invalid case, and Lint should catch this
        'c': 3,
    })


func()

Accessing (reading) the module level variables is a common pattern that we use, however updating a module level (global) variables is an unsafe operation. I have tried using the ast library and the libCST library to catch this updation. But I couldn't find any way of detecting mutation of a global variable. The only way I could think of is hard coding a list of update operations for mutable data structures, like .extend(), .append(), etc for list data structure; and .update(), .pop(), etc for dict data structure. And while traversing the tree (AST or CST), store a list of all the global variables if they are mutable type, and if one of the mentioned method is called on one of them, the lint rule can call it out. But this doesn't look like a full proof solution.

Can you please tell ideas around how can I implement a custom lint rule for this?

1 Answers

You could make the global en immutable data structure, like this:

class Person:
    """Immutable person class"""

    # Using __slots__ reduces memory usage.
    # If __slots__ doesn't include __dict__, new attributes cannot be added.
    # This is not always desirable, e.g. it you want to subclass Person.
    __slots__ = ("name", "age")

    def __init__(self, name, age):
        """Create a Person instance.

        Arguments:
            name (str): Name of the person.
            age: Age of the person. Must be convertible to a float.
        """
        # Parameter validation. This shows how to do this,
        # but you don't always want to be this inflexibe.
        if not isinstance(name, str):
            raise ValueError("'name' must be a string")
        # Use super to get around __setattr__ definition
        super(Person, self).__setattr__("name", name)
        try:
            # A little more flexible parameter validation.
            super(Person, self).__setattr__("age", round(float(age)))
        except ValueError:
            raise ValueError("'age' cannot be converted to int")

    def __setattr__(self, name, value):
        """Prevent modification of attributes."""
        raise AttributeError(f"{type(self)} cannot be modified")

    def __repr__(self):
        """Create a string representation of the Person.
        You should always have at least __repr__ or __str__
        for interactive use.
        """
        return f'<Person(name="{self.name}", age={self.age})>'

You can create instances of this type:

In [2]: PERSON = Person("John Doe", 37)
Out[2]: <Person(name="John Doe", age=37)>

But you cannot easily modify them:

In [3]: PERSON.name = "Foo Bar"
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-139d70f50d64> in <cell line: 1>()
----> 1 PERSON.name = "Foo Bar"

<ipython-input-1-41ab3e15b161> in __setattr__(self, name, value)
     28     def __setattr__(self, name, value):
     29         """Prevent modification of attributes."""
---> 30         raise AttributeError(f"{type(self)} cannot be modified")
     31 
     32     def __repr__(self):

AttributeError: <class '__main__.Person'> cannot be modified

(You can actually modify them using super, as shown in Person.__init__(). But this at least protects you from casual mistakes.)

This of course cannot prevent you from assigning another object to PERSON! That is not possible in Python. And it would basically require a modification of the language to make that possible.

There is the convention that variables named in ALLCAPS are constants, but that's just a convention. To the best of my knowledge, this cannot be enforced.

Edit1

Python has existed for decades with the convention that ALL_CAPS variables are constants. Apparently this has worked well enough that a change in the language to include say a const statement has not been deemed necessary.

Related