Python — OOP Part 3: Class Variable

8 min read

In the previous lesson we covered the concept of an object — defining a class, creating instances, and using instance methods and instance variables via self. In this lesson we’ll look at class variables, a slightly different concept from instance variables.

What is a class variable? #

While an instance variable is unique data that each instance carries (like a person’s name), a class variable is data shared by every instance created from the same class — like an organization’s name shared by all its members.

Suppose a company gives its employees a salary raise once a year, and unusually, applies the same raise rate to every employee. This year revenue was strong, so everyone gets a 10% raise. (Sounds like a dream, right? 😄) The common raise rate that applies to all employees is a perfect candidate for a class variable. Let me explain with an example.

In a directory of your choice, create a file named oop_3.py, save the code below, and run it.

oop_3.py
class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * 1.1)  #1 raise salary by 10%

emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# original salary')
print(emp_1.pay)  # original salary

print('\n# apply raise')
emp_1.apply_raise()  # apply raise

print('\n# raised salary')
print(emp_1.pay)  # raised salary
Console output
# original salary
50000

# apply raise

# raised salary
55000

On line 12 (#1), 1.1 — i.e. a 10% raise — is applied and the salary goes up. But this code is a bad example because the raise rate is hard-coded. What if 1.1 is used not in just one place but several? Every time the rate changes, you’d have to update every spot, and missing even one could cause serious bugs. The ideal pattern is to assign the value to a variable, reference the variable wherever needed, and change just the variable when the value changes.

Let’s rewrite the code using a class variable.

oop_3.py
class Employee:
    raise_amount = 1.1  # 1 define class variable

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * raise_amount)  # 2 use class variable


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# original salary')
print(emp_1.pay)  # original salary

print('\n# apply raise')
emp_1.apply_raise()  # apply raise

print('\n# raised salary')
print(emp_1.pay)  # raised salary
Console output
# original salary
50000

# apply raise
Traceback (most recent call last):
File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 24, in <module>
emp_1.apply_raise()  # apply raise
File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 14, in apply_raise
self.pay = int(self.pay * raise_amount)  # 2 use class variable
NameError: name 'raise_amount' is not defined

We defined a class variable on line 2 (#1) and referenced it on line 14 (#2). Oh… we got a NameError saying raise_amount is not defined. Why?! The class variable lives in the class namespace, so we have to access it through the class. Let’s fix it.

oop_3.py
class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * Employee.raise_amount)  # 1 access via the Employee class


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# original salary')
print(emp_1.pay)  # original salary

print('\n# apply raise')
emp_1.apply_raise()  # apply raise

print('\n# raised salary')
print(emp_1.pay)  # raised salary
Console output
# original salary
50000

# apply raise

# raised salary
55000

On line 14 (#1) we accessed raise_amount through the Employee class. It runs without issue.

Object namespace #

Can we also access raise_amount through an instance via self, instead of Employee? Surely not… it’s a class variable, how could self reach it? Let’s try.

oop_3.py
class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 access via the instance (self)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# original salary')
print(emp_1.pay)  # original salary

print('\n# apply raise')
emp_1.apply_raise()  # apply raise

print('\n# raised salary')
print(emp_1.pay)  # raised salary
Console output
# original salary
50000

# apply raise

# raised salary
55000

Wait, what?! It worked even through the instance. Why??? Python has a namespace structure that looks like the diagram below. The namespace organizes object names hierarchically, and when looking up a name it searches in this order: instance namespaceclass namespacesuperclass namespace. But not the other way around — a child can reference a parent’s namespace, but a parent cannot reference a child’s. So when we use self.raise_amount, Python first looks for raise_amount in the instance namespace, doesn’t find it, and then looks in the class namespace.

Python object name resolution order
Python object name resolution order

Let’s use the __dict__ attribute to peek inside the class and instance namespaces.

oop_3.py
class Employee:
    raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# instance namespace')
print(emp_1.__dict__)

print('\n# class namespace')
print(Employee.__dict__)
Console output
# instance namespace
{'first': 'Sanghee', 'last': 'Lee', 'pay': 50000, 'email': 'sanghee.lee@schoolofweb.net'}

# class namespace
{'__module__': '__main__', 'raise_amount': 1.1, '__init__': <function Employee.__init__ at 0x000001E37154F790>, 'full_name': <function Employee.full_name at 0x000001E37154F820>, 'apply_raise': <function Employee.apply_raise at 0x000001E37154F8B0>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>, '__doc__': None}

As the output shows, raise_amount does not exist in the emp_1 instance namespace; it lives only in the Employee class object’s namespace. The next example will make it even clearer.

oop_3.py
class SuperClass:
    super_var = 'A variable in the superclass namespace.'


class MyClass(SuperClass):
    class_var = 'A variable in the class namespace.'

    def __init__(self):
        self.instance_var = 'A variable in the instance namespace.'


my_instance = MyClass()

# Accessible
print('my_instance.instance_var')
print(my_instance.instance_var)
print('\nmy_instance.class_var')
print(my_instance.class_var)
print('\nmy_instance.super_var')
print(my_instance.super_var)
print('\nMyClass.class_var')
print(MyClass.class_var)
print('\nMyClass.super_var')
print(MyClass.super_var)
print('\nSuperClass.super_var')
print(SuperClass.super_var)
print('-' * 30)

# Not accessible
try:
    print(SuperClass.class_var)
except:
    print('SuperClass.class_var')
    print('class_var not found...')

try:
    print(MyClass.instance_var)
except:
    print('\nMyClass.instance_var')
    print('instance_var not found...')
Console output
my_instance.instance_var
A variable in the instance namespace.

my_instance.class_var
A variable in the class namespace.

my_instance.super_var
A variable in the superclass namespace.

MyClass.class_var
A variable in the class namespace.

MyClass.super_var
A variable in the superclass namespace.

SuperClass.super_var
A variable in the superclass namespace.
------------------------------
SuperClass.class_var
class_var not found...

MyClass.instance_var
instance_var not found...

That should make things much clearer. Let’s go back to the first example.

The company’s policy of applying the same raise rate to every employee has changed. Only Sanghee Lee, this year’s top performer, will receive a special 20% raise. 😭 Thank you… haha. In a case like this, can we still use a class variable? Yes — use both a class variable and an instance variable. Look at the code below.

oop_3.py
class Employee:
    raise_amount = 1.1  # class variable applied to all employees

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 look up the instance variable first


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

emp_1.raise_amount = 1.2  # instance variable for the special raise

print('# emp_1: 20% raise')
print(emp_1.pay)
emp_1.apply_raise()
print(emp_1.pay)
print('# emp_2: 10% raise')
print(emp_2.pay)
emp_2.apply_raise()
print(emp_2.pay)
Console output
# emp_1: 20% raise
50000
60000
# emp_2: 10% raise
60000
66000

As shown above, on line 14 (#1) we use self, so the lookup starts at the instance variable namespace. emp_1 has raise_amount in its instance namespace, so that one gets used; emp_2 doesn’t have it, so Python automatically falls back to the class variable raise_amount.

Now let’s look at another good case for class variables.

Suppose we need to track the total number of employees in the company. Each individual employee doesn’t need to carry this data — better to store and reference it in one place, right? Look at the code below.

oop_3.py
class Employee:

    raise_amount = 1.1
    num_of_emps = 0  # 1 define class variable

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

        Employee.num_of_emps += 1  # 2 increment by 1 each time an instance is created

    def __del__(self):
        Employee.num_of_emps -= 1  # 3 decrement by 1 each time an instance is removed

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 look up the instance variable first


print(Employee.num_of_emps)  # initial employee count
emp_1 = Employee('Sanghee', 'Lee', 50000)  # one employee joins
emp_2 = Employee('Minjung', 'Kim', 60000)  # one employee joins
print(Employee.num_of_emps)  # employee count

del emp_1  # one employee leaves
del emp_2  # one employee leaves
print(Employee.num_of_emps)  # employee count
Console output
0
2
0

On line 21 (#1) we defined a class variable num_of_emps, and inside __init__ (#2) — which runs whenever an instance is created — we increment num_of_emps by 1. At #3 we use the destructor __del__ to decrement num_of_emps by 1 whenever an instance is removed. This is how class variables let you manage data cleanly in situations where instance variables would be cumbersome. Conceptually, it’s similar to a global variable in regular functions.

This post covered when to use instance variables versus class variables. In addition to instance methods, Python also provides class methods and static methods, which the next post covers.

X