* Python: Classes with Mutable Attributes
*
* Date: 14.07.2024
*\
You might have heard, that in Python "everything is an object", and indeed also classes themselves are in fact (mutable) objects. Classes being objects themselves makes Python's object model very flexible and allows for the generation and modification of classes at runtime. In this article, we are first going to demonstrate, that classes are indeed mutable objects and then show the effects and pitfalls of classes and objects containing mutable attributes.
Let's define a class "ClassA" and a variable "ClassB", that references the very same class object.
elem_bool_ = False
ClassB = ClassA
It will *not be surprising to you that a subsequent check on object identity
of the classes "ClassA" and "ClassB" yields "True". Now, when we change "elem_bool_" in our original class definition "ClassA"
a request of the corresponding attribute of "ClassA" and "ClassB" will both yield "True". This demonstrates that both "ClassA" and "ClassB" continue to reference the same object, that this object has been changed by our assignment and that as a consequence it must be mutable. In contrast, if class objects were immutable, the class object targeted by "ClassA" would have been replaced and "ClassB" would still point to the old class object. Similar to the *previous article, we can depict this situation as in Figure 1.

The dir function allows you to see the valid attributes and methods of a given object and, when applied to the object "ClassA",
will yield
'__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__',
'elem_bool_']
which shows, that "elem_bool_" appears just as a regular attribute of the class object and that the class object does not look any different from other objects.
Similar to the preceding articles, let's now define a new class "ClassC" with an attribute, that is itself mutable.
elem_list_ = [1, 2, 3]
The class "ClassC" from Listing 2 can subsequently be instantiated in the well known way
ObjC_1 = ClassC()
and the two resulting objects "ObjC_0" and "ObjC_1" contain the element "elem_list_" as expected. Suppose now, we forward references to these objects throughout a program of ours, and at some point decide that the value of one of the elements of "elem_list_" has to be changed. The code snippet
print(ObjC_0.elem_list_)
would then yield
which is exactly what we wanted. However, we have created a huge problem, because
and even worse
both now yield
as well. It is obvious that class instantiation creates a reference to the attribute "elem_list_" of "ClassC" and adds this reference to any newly created object based on the given class. By changing the mutable attribute "elem_list_" of the object "ObjC_0" we have thus changed the other instance of "ClassC", "ObjC_1", as well and at the same time we have corrupted the definition of "ClassC" itself such that after this point in the program any new instances of "ClassC" will also be initialized with "ObjC_0's" changed value. The resulting situation is shown in Figure 2

from where it is clear that any access to the attribute "elem_list_" from the definition of the class "ClassC" or any one of its instances will further corrupt the original definition and that this can easily be a source of hard to find bugs.
In most cases, you want to avoid the situation described in the previous section, which is easily done using the object constructor "__init__". Either you redefine "ClassC" using "copy" or "deepcopy" (depending on your use case, see *here) as in Listing 4
class ClassC:
elem_list_ = [1, 2, 3]
def __init__(self):
self.elem_list_ = copy.deepcopy(self.elem_list_)
or, perhaps a more elegant solution, you use the model provided in Listing 5
class ClassC:
def __init__(self):
self.elem_list_ = [1, 2, 3]
where the target object for the attribute "elem_list_" is created only in the constructor and added dynamically to the object. After having defined "ClassC" as in Listing 4 you can execute
ObjC_1 = ClassC()
again, which will subsequently lead to the to the situation depicted in Figure 3.

Now you can modify "ObjC_0" without affecting "ObjC_1" and the original class definition "ClassC":
print(ObjC_0.elem_list_)
will result in
while
and
will still yield
and thus preserve the attribute's value as intended. The class object "ClassC" from Listing 5 does not contain any such element by its definition, so there is no way to corrupt it in the class object from the beginning. By creating a new list object in the constructor each time an object is created based on "ClassC", which is then assigned to "elem_list_", all the "elem_list_" attributes are decoupled as well and you will get the same behavior as with "ClassC" defined as in Listing 4.
It is important to consider, that even though "elem_list_" is of the type "list", the problems pointed out in this article are entirely caused by the fact that the attribute is of a mutable type. Consequently, the inconvenient behavior described in this article extends to all mutable types, which notably includes all user-defined classes or objects based on them used as attributes in a given class definition. The two mitigation procedures presented in Listing 4 and Listing 5 can be employed to eliminate the corruption of mutable attributes in class definitions; in order to use them purpusefully it pays off to know which types are mutable and which are not. If you are unsure about this, you can read up on this topic *here.