We look at some behaviours of class attributes which can help to make life easier for us – mere programmers.

054-feature-image.png
Python: class attributes, some behaviours we should be aware of.

We will look at the following three (3) behaviours:

  1. From the Python official documentation:

    9.4. Random Remarks

    If the same attribute name occurs in both an instance and in a class, then attribute lookup prioritizes the instance.

    https://docs.python.org/3/tutorial/classes.html:
  2. Setting the value of a class attribute via the class will propagate the new value to class instances, whom have not overridden the value of this class attribute. This is in conformance with the documentation above.
  3. Setting the value of a class attribute via the class will propagate the new value down to child classes, but no vice versa.

Let’s explore these points via examples.

❶ Attribute lookup prioritises the instance.

This is an example from the documentation page quoted above, I have tweaked it a tiny bit.

class Warehouse:
    purpose = 'Storage'
    region = 'west'

Then:

w1 = Warehouse()
print("1: ", w1.purpose, w1.region)

Output – these are default class attributes’ values:

1:  Storage west
w2 = Warehouse()
w2.region = 'east'
print("2: ", w2.purpose, w2.region)

We just instantiate an instance of Warehouse, then override the value of the region attribute with 'east' (*):

2:  Storage east

(*): please note, what I’ve just written above might not be correct… According to the quoted documentation, the statement w2.region = 'east' might actually mean assigning the new attribute region to instance w2, rather than override as I’ve written.

❷ Setting the value via class propagates the new value to instances whom have not provided their own value.

We continue with examples in ❶:

Warehouse.region = 'north'
w3 = Warehouse()
print("3: ", w3.purpose, w3.region)

Instance w3 is created with whatever the class attributes’ values of Warehouse class:

3:  Storage north

Setting Warehouse.region = 'north', how does this affect the existing two (2) instances w1 and w2?

print(f"4: w1.region: {w1.region}, w2.region: {w2.region}")
4: w1.region: north, w2.region: east

w1 has not set its own value for the region attribute, setting the new value via the class Warehouse does propagate back to instance w1. w2, on the hand, has set its own, so it was not affected.

❸ Setting the value propagates from the parent class to child classes, but not vice versa.

Consider the following classes:

class Engine:
    started = False;

class TwoStrokeEngine(Engine):
    pass
	
class FourStrokeEngine(Engine):
    pass

In their initial state, started is False for all classes:

print(f"1. Engine.started: {Engine.started}")
print(f"1. TwoStrokeEngine.started: {TwoStrokeEngine.started}")
print(f"1. FourStrokeEngine.started: {FourStrokeEngine.started}\n")
1. Engine.started: False
1. TwoStrokeEngine.started: False
1. FourStrokeEngine.started: False

Let’s set Engine.started to True:

Engine.started = True

print(f"2. Engine.started: {Engine.started}")
print(f"2. TwoStrokeEngine.started: {TwoStrokeEngine.started}")
print(f"2. FourStrokeEngine.started: {FourStrokeEngine.started}\n")
2. Engine.started: True
2. TwoStrokeEngine.started: True
2. FourStrokeEngine.started: True

Let’s switch Engine.started back to False:

Engine.started = False

print(f"3. Engine.started: {Engine.started}")
print(f"3. TwoStrokeEngine.started: {TwoStrokeEngine.started}")
print(f"3. FourStrokeEngine.started: {FourStrokeEngine.started}\n")
3. Engine.started: False
3. TwoStrokeEngine.started: False
3. FourStrokeEngine.started: False

Let’s set FourStrokeEngine.started to True:

FourStrokeEngine.started = True

print(f"4. Engine.started: {Engine.started}")
print(f"4. TwoStrokeEngine.started: {TwoStrokeEngine.started}")
print(f"4. FourStrokeEngine.started: {FourStrokeEngine.started}\n")
4. Engine.started: False
4. TwoStrokeEngine.started: False
4. FourStrokeEngine.started: True

– We can see that, setting the value propagates from the parent class to child classes, but not vice versa.

What about their instances? Continue on with the examples above:

"""
FourStrokeEngine.started is True from above.
"""

engine = Engine()
two_stroke_engine = TwoStrokeEngine()
four_stroke_engine = FourStrokeEngine()
four_stroke_engine1 = FourStrokeEngine()

print(f"5. engine.started: {engine.started}")
print(f"5. two_stroke_engine.started: {two_stroke_engine.started}")
print(f"5. four_stroke_engine.started: {four_stroke_engine.started}")
print(f"5. four_stroke_engine1.started: {four_stroke_engine1.started}\n")

Engine.started = True

print(f"6. engine.started: {engine.started}")
print(f"6. two_stroke_engine.started: {two_stroke_engine.started}")
print(f"6. four_stroke_engine.started: {four_stroke_engine.started}")
print(f"6. four_stroke_engine1.started: {four_stroke_engine1.started}\n")

Output:

5. engine.started: False
5. two_stroke_engine.started: False
5. four_stroke_engine.started: True
5. four_stroke_engine1.started: True

6. engine.started: True
6. two_stroke_engine.started: True
6. four_stroke_engine.started: True
6. four_stroke_engine1.started: True

Let’s set TwoStrokeEngine.started to False, and see what happens to existing instances:

TwoStrokeEngine.started = False

print(f"7. engine.started: {engine.started}")
print(f"7. two_stroke_engine.started: {two_stroke_engine.started}")
print(f"7. four_stroke_engine.started: {four_stroke_engine.started}")
print(f"7. four_stroke_engine1.started: {four_stroke_engine1.started}\n")
7. engine.started: True
7. two_stroke_engine.started: False
7. four_stroke_engine.started: True
7. four_stroke_engine1.started: True

It makes sense that only two_stroke_engine.started was affected.

I did get caught out on some of these issues… And hence this post. I do hope you find this post useful. Thank you for reading and stay safe as always.