Class Vs Instance variable in Python: Many know but few understand.

So before I begin talking, I have a question for you, just don't scroll below & look at the code snippet given below and try to answer this question without running the code,

"What is the output of print(t2.foo)?"

class FooBar:
    foo = []

    def __init__(self):
        self.bar = ['bar']

t1 = FooBar()

t1.foo.append(2)

t2 = FooBar()

print(t2.foo)

# What is the output here?

So did you get the answer? Is it [] (empty list)? You cheated & ran the code & shocked to see the output as [2]. If you are wondering why the answer is this then this article is for you.

What are the "class" & "instance" variables?

In simple words, a class variable is a variable defined in the class which can be accessed directly using the class whereas an instance variable is a variable defined in the class which is tied to a particular instance of that class.

class FooBar:
    foo = [] # <- class variable

    def __init__(self):
        self.bar = ['bar'] # <- instance variable

Now the thing is that all the instances here will have access to foo but foo can also be accessed as a class property directly here like this

FooBar.foo # Gives [] list

# This works too

t1 = FooBar()

t1.foo

To understand how this is working we need to understand the namespaces in Python.

Python's namespaces

A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries - Python docs

So it basically means objects are given names in the namespace that is uniquely available in the current namespace. You generally use . notation for accessing values using names in the namespace.

class MyClass:
    class_var = 1

    def __init__(self):
        pass


# Using . to access the variable from the class's namespace
MyClass.class_var

Python classes and instances each have their own distinct namespaces represented by .__dict__ attribute. You can access their respective namespaces like this.

# To access all the mapped values in the class's namespace
MyClass.__dict__

# To access all the mapped values in the object's namespace
obj = MyClass()

obj.__dict__

So when you try to access a variable in Python, first it looks in the object's namespace, if it can't find that in the object's namespace then it goes one level up and looks in the class's namespace. If it's not there also then it will throw an error saying that the attribute is not available.

What if you try to change the value?

So far we have seen & understood how accessing a class or instance variables works but let's understand what happens when you try to assign or re-assign a value to it.

So when you assign a value to a class variable or re-assign it then the value will be available in all the instances of that class.

class MyClass:
    class_var = 1

t = MyClass()

MyClass.class_var = 2

print(t.class_var) # prints 2

So in the above code, I have created a class with a class variable called class_var. I created an object of that class called t. After that I changed the class variable value to 2, now if you try to print that value then you will get the 1 as output but 2 because that has been overridden by the class variable.

If I do this assignment with the instance variable then the instance variable overrides the class variable but only for that instance

class MyClass:
    class_var = 1

t = MyClass()

t.class_var = 3

print(t.class_var)

This becomes more complicated if we involve mutable data types such as a list. See this example below

class FooBar:
    foo = []

    def __init__(self):
        self.bar = ['bar']


t1 = FooBar()

t1.foo.append(2)

t2 = FooBar()

print(t2.foo) # We expect [] but get ['2'] as output

Though we are appending value in the list of instance t1 but that mutates that list for instance t2 as well. This can cause bugs in your program & you will start pulling your hair while debugging it if you do not understand it.

So what the heck is happening here? This is the behavior of mutable data types because Python handles the assignment by reference, not by making a copy of it.

You can verify that both foo variables of instance t1 & t2 are pointing to the same memory address.

print(id(t1.foo) == id(t2.foo)) # True

So if you make changes in one instance it will be reflected in other instances of that class. So one way to solve this problem is initializing the variable in the __init__ method so that every time you create a new instance you get a fresh copy of that list.

class FooBar:
    def __init__(self):
        self.foo = []

t1 = FooBar()

t1.foo.append(2)

t2 = FooBar()

print(t2.foo) # We expects [] & get [] as output

print(id(t1.foo) == id(t2.foo)) # False

So I conclude here, be cautious while declaring the class variable, most of the time you don't need it & if you need then be aware of this behavior of Python.

I hope you enjoyed the article & learned something new today...

🤝  Connect with Me

I share more Python & coding tips & tricks on my social media. Drop a "Hi" if you want.