29. Class Attributes#

In our earlier lesson, we got to learn about instance attributes. Here let’s learn about class Attributes 😎

Class Attributes are cousins of Instance Attributes 😬. Just kidding.

As Instance attributes are bound to instances/objects, Class attributes are bound to the Class itself.

Class vs Instance attributes

Code speaks louder than words 🥁. Let’s get into an example 🙂:

class SuperMarket:
    discount = 0  # Here's the class attribute.

    def __init__(self, item_price):
        self.item_price = item_price

    def get_bill(self):
        return self.item_price - ((SuperMarket.discount / 100) * self.item_price)

We have created a class SuperMarket and it contains a variable called discount, well this is the Class Attribute that we are going to explore.

As we learned that Class Attributes are bound to class whereas Instance Attributes are bound to instance, lets explore it first.

Checking if Instance attribute is bound to Class?

SuperMarket.item_price
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 SuperMarket.item_price

AttributeError: type object 'SuperMarket' has no attribute 'item_price'

item_price is Instance Attribute, We can clearly see that there’s an AttributeError raised when trying to access instance attribute using the Class rather than the instance.

Checking if Class Attribute is bound to Class?

SuperMarket.discount
0

🎊 Hurray! We are able to access discount using the Class itself.

Hence, Class Attributes are bound to Class whereas Instance Attributes are bound to Instances.

Well, there’s no reason for you to trust me 😬, So let’s look at the attributes present for the class.

SuperMarket.__dict__
mappingproxy({'__module__': '__main__',
              'discount': 0,
              '__init__': <function __main__.SuperMarket.__init__(self, item_price)>,
              'get_bill': <function __main__.SuperMarket.get_bill(self)>,
              '__dict__': <attribute '__dict__' of 'SuperMarket' objects>,
              '__weakref__': <attribute '__weakref__' of 'SuperMarket' objects>,
              '__doc__': None})

We can see that SuperMarket.__dict__ returned an object which shows the attribute discount. Hence, discount is a class attribute as it is bound to its class 😊.

29.1. 🔔 Altering the class Attributes#

First of all, let’s check what is the value of discount?

SuperMarket.discount
0

Let’s create an object of SuperMarket

obj = SuperMarket(100)  # Creation of object of the class SuperMarket.
obj.discount
0

We can still see the value of discount of the obj to be 0 😊.

Let’s change the discount class attribute through the class SuperMarket.

SuperMarket.discount = 10

We have changed the value of the class attribute discount to 10.

SuperMarket.discount
10
obj.discount
10

We can see that changing the class attribute values changes the values of the attributes of the object as well.

But, what if we change the class attribute via the object? Does it changes the actual Class Attribute? 🤔”

SuperMarket.discount
10
obj.discount
10

At present, we have both SuperMarket.discount and obj.discount values to be 10.

obj.discount = 20  # Changing the object's discount value to 20.
obj.discount  # Checking if the object's discount changed to 20?
20
SuperMarket.discount  # Checking for the class Attribute 'discount' bound to the class.
10

Well, We got to see that when the object’s class attribute value is changed, it doesn’t change the Class’s class attribute value 😬. Well, what’s the reason for this behaviour 🤔?

To identify what paved the way for the above behaviour, let’s create another object of the class SuperMarket

obj2 = SuperMarket(100)  # Creating a new object.

Now let’s look at the instance attributes of obj2 by using the special/magic method __dict__.

obj2.__dict__
{'item_price': 100}

We see that obj2 has only item item_price. let’s look at obj also on which we tried to change the value of class attribute discount.

obj.__dict__
{'item_price': 100, 'discount': 20}

There’s an extra field discount for obj, it’s because we tried to change the class attribute’s value which actually shadows/creates a new attribute with the same name as instance attribute

🤔 How to revert the value of discount to be the class attribute’s value, which in our case should be 10.

Easy Peasy! we just need to delete the attribute of the obj using del.

del obj.discount

Now, let’s look at the value of obj.discount which was earlier 20.

obj.discount
10

Hurray! we are back to square one. By deleting the shadowed discount instance attribute, we are back with the Class Attribute discount whose value is 10.

Important

Modifying the Class’s class attribute changes the attribute in all the objects created. But, changing the class attribute bound to the instance/object shadows the class attribute by creating an instance attribute of same name which is bound only to the objectclass.

29.2. Accessing class attributes within the class.#

Well, we got to learn about class attributes, but are they accessible within the class 🤔?

Definitely, they are accessible within the class. Let’s write a new class for the demonstration.

class Counter:
    no_of_objects_created = 0

    def __init__(self):
        Counter.no_of_objects_created += 1
        print(
            f"Number of objects created of Counter class are: {Counter.no_of_objects_created}"
        )

We have created a class called Counter, its only purpose here is to count the number of objects created out of it. no_of_objects_created is the class attribute here. Initially it’s value is 0, for every object created of Counter class, no_of_objects_created gets increased by 1.

Counter.no_of_objects_created
0
counter_1 = Counter()
counter_2 = Counter()
Number of objects created of Counter class are: 1
Number of objects created of Counter class are: 2

🏆 We did it, we created a class Counter which counts number of objects created.

In the above example to access the class attribute we used Counter.no_of_objects_created which is <Class>.<class_attribute>, There’s another way to access it using self. Let’s give it a try:

class NewCounter:
    no_of_objects_created = 0

    def __init__(self):
        self.no_of_objects_created += 1
        print(
            f"Number of objects created of Counter class are: {self.no_of_objects_created}"
        )
new_counter_1 = NewCounter()
new_counter_2 = NewCounter()
Number of objects created of Counter class are: 1
Number of objects created of Counter class are: 1

😰 Did it work? No❌, We see that the output is always 1, The culprit here is again Shadowing of class attributes as instance attributes

Note

So should we always use <Class>.<class_attribute> instead of self.<class_attribute> 🤔? We can use either way while reading the class attribute. But, If we want to alter the class attribute, we should be using <Class>.<class_attribute> to cascade the change to all the objects.

There’s no advantage of using class attributes if there are chances of altering only for a particular instance using self, so hard rule is use <Class>.<class_attribute> for accessing the class within the class 😤.