\*
 * 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.

// Mutability and Classes

Let's define a class "ClassA" and a variable "ClassB", that references the very same class object.

class ClassA:
  elem_bool_ = False

ClassB = ClassA

Listing 1: Definition of "ClassA" and creation of a reference to "ClassA" called "ClassB".

It will *not be surprising to you that a subsequent check on object identity

ClassA is ClassB

of the classes "ClassA" and "ClassB" yields "True". Now, when we change "elem_bool_" in our original class definition "ClassA"

ClassA.elem_bool_ = True

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.

A class with an immutable attribute referenced by two variables.

Figure 1: Situation created by Listing 1.

The dir function allows you to see the valid attributes and methods of a given object and, when applied to the object "ClassA",

dir(ClassA)

will yield

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
 '__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.

// Mutable Attributes

Similar to the preceding articles, let's now define a new class "ClassC" with an attribute, that is itself mutable.

class ClassC:
  elem_list_ = [1, 2, 3]

Listing 2: Definition of "ClassC" with an attribute referencing a mutable object.

The class "ClassC" from Listing 2 can subsequently be instantiated in the well known way

ObjC_0 = ClassC()
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

ObjC_0.elem_list_[0] = 5
print(ObjC_0.elem_list_)

Listing 3: Changing the mutable attribute and printing its value to the screen.

would then yield

[5, 2, 3]

which is exactly what we wanted. However, we have created a huge problem, because

print(ObjC_1.elem_list_)

and even worse

print(ClassC.elem_list_)

both now yield

[5, 2, 3]

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

The situation created by Listing 3.

Figure 2: Situation created by Listing 3.

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.

// Ensuring Attribute Integrity

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

import copy

class ClassC:
  elem_list_ = [1, 2, 3]
  def __init__(self):
    self.elem_list_ = copy.deepcopy(self.elem_list_)

Listing 4: Ensuring attribute integrity using "deepcopy".

or, perhaps a more elegant solution, you use the model provided in Listing 5

import copy

class ClassC:
  def __init__(self):
    self.elem_list_ = [1, 2, 3]

Listing 5: Ensuring attribute integrity by generating the target object for the attribute in the constructor.

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_0 = ClassC()
ObjC_1 = ClassC()

again, which will subsequently lead to the to the situation depicted in Figure 3.

The situation created by Listing 4.

Figure 3: Situation created by Listing 4.

Now you can modify "ObjC_0" without affecting "ObjC_1" and the original class definition "ClassC":

ObjC_0.elem_list_[0] = 5
print(ObjC_0.elem_list_)

will result in

[5, 2, 3]

while

print(ObjC_1.elem_list_)

and

print(ClassC.elem_list_)

will still yield

[1, 2, 3]

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.

// Conclusion

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.