Source: Sven Brandsma on Unsplash
Expansion: Until now, we've primarily focused on instance variables, which are unique to each individual object (instance) created from a class. Think of them as personal belongings—each person has their own unique set.
Class variables, on the other hand, are attributes that belong to the class itself, not to any specific instance. They are shared among all instances of that class. Imagine them as shared resources or universal facts about a category. For example, if you have a Car
class, number_of_wheels
(assuming it's always 4) would be a good candidate for a class variable because all cars generally have the same number of wheels.
Feature | Instance Variables | Class Variables |
---|---|---|
Ownership | Owned by individual instances. | Owned by the class itself. |
Scope | Unique to each instance. | Shared by all instances of the class. |
Definition | Defined inside methods (usually __init__ ) using self.attribute_name . |
Defined directly within the class body, outside any methods. |
Purpose | Store data unique to each object. | Store data common to all objects of the class, or class-wide constants/counters. |
The way Python handles attribute lookups (whether it's an instance variable or a class variable) is important to understand:
Instance First: When you try to access an attribute using an instance (e.g., employee_1.raise_amount
), Python first checks the instance's dictionary (employee_1.__dict__
) to see if that attribute exists as an instance variable.
Class Second: If the attribute is not found as an instance variable on that specific instance, Python then looks up the attribute in the class's dictionary (Employee.__dict__
). If it finds the attribute there, it's treated as a class variable.
This lookup order explains why you can access class variables through an instance (employee_1.raise_amount
) even though they are technically defined on the class. It also sets the stage for understanding the behavior when modifying class variables.
Example with Employee
Class:
class Employee:
# Class variables
raise_amount = 1.04 # All employees get a 4% raise by default
num_of_emps = 0 # Tracks the total number of employees
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first + '.' + last + '@company.com'
# When a new employee is created, increment the class variable
Employee.num_of_emps += 1 # Access using the class name
# Create instances
emp_1 = Employee('Ryan', 'McBride', 50000)
emp_2 = Employee('Test', 'User', 60000)
print(f"Employee 1 Pay: {emp_1.pay}")
print(f"Employee 2 Pay: {emp_2.pay}")
# Accessing class variables:
print(f"
Accessing raise_amount:")
print(f"Using class name: {Employee.raise_amount}") # Recommended way for class variables
print(f"Using instance emp_1: {emp_1.raise_amount}")
print(f"Using instance emp_2: {emp_2.raise_amount}")
print(f"
Number of Employees: {Employee.num_of_emps}")
Output Explanation:
Employee.raise_amount
directly accesses the class variable.emp_1.raise_amount
and emp_2.raise_amount
also access the class variable because neither emp_1
nor emp_2
has an instance variable named raise_amount
. Python falls back to looking at the Employee
class.num_of_emps
is correctly incremented every time a new Employee
instance is created, demonstrating its shared nature.This is where the behavior of class variables can be counter-intuitive if you're not aware of Python's attribute lookup rules.
When you modify a class variable using the class name, you are directly changing the attribute on the class object itself. This change will be reflected across all instances of that class (both existing and future ones) because they all refer to the same class attribute.
class Employee:
raise_amount = 1.04
num_of_emps = 0
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first + '.' + last + '@company.com'
Employee.num_of_emps += 1
emp_1 = Employee('Ryan', 'McBride', 50000)
emp_2 = Employee('Test', 'User', 60000)
print(f"Initial raise_amount (emp_1): {emp_1.raise_amount}")
print(f"Initial raise_amount (emp_2): {emp_2.raise_amount}")
print(f"Initial raise_amount (Class): {Employee.raise_amount}")
# Modify raise_amount using the class name
Employee.raise_amount = 1.05 # A company-wide change
print(f"
After modifying via class name:")
print(f"New raise_amount (emp_1): {emp_1.raise_amount}") # Now 1.05
print(f"New raise_amount (emp_2): {emp_2.raise_amount}") # Now 1.05
print(f"New raise_amount (Class): {Employee.raise_amount}") # Now 1.05
Explanation: By changing Employee.raise_amount
, you directly altered the shared blueprint. Therefore, when emp_1
and emp_2
subsequently try to access raise_amount
, they don't find it on themselves and look up to the class, finding the new value of 1.05
.
This is the tricky part. When you try to modify a class variable using an instance, Python does not modify the class variable. Instead, due to the attribute lookup order, it creates a new instance variable with the same name on that specific instance. This new instance variable then "shadows" the class variable for that instance.
The original class variable remains unchanged, and other instances of the class will still refer to the original class variable.
class Employee:
raise_amount = 1.04 # Class variable
num_of_emps = 0
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first + '.' + last + '@company.com'
Employee.num_of_emps += 1
emp_1 = Employee('Ryan', 'McBride', 50000)
emp_2 = Employee('Test', 'User', 60000)
print(f"Initial raise_amount (emp_1): {emp_1.raise_amount}")
print(f"Initial raise_amount (emp_2): {emp_2.raise_amount}")
print(f"Initial raise_amount (Class): {Employee.raise_amount}")
# Modify raise_amount using an instance
emp_1.raise_amount = 1.10 # This creates an instance variable on emp_1
print(f"
After modifying via instance (emp_1):")
print(f"New raise_amount (emp_1): {emp_1.raise_amount}") # Now 1.10 (instance variable)
print(f"New raise_amount (emp_2): {emp_2.raise_amount}") # Still 1.04 (class variable)
print(f"New raise_amount (Class): {Employee.raise_amount}") # Still 1.04 (class variable)
# Let's inspect the __dict__ to confirm
print(f"
emp_1.__dict__: {emp_1.__dict__}")
print(f"emp_2.__dict__: {emp_2.__dict__}")
print(f"Employee.__dict__: {Employee.__dict__}")
Output Explanation:
emp_1.raise_amount
now prints 1.10
because emp_1
now has its own raise_amount
instance variable. When you call emp_1.raise_amount
, Python finds this instance variable first and uses it.emp_2.raise_amount
still prints 1.04
because emp_2
does not have an instance variable named raise_amount
, so it falls back to the class variable.Employee.raise_amount
also still prints 1.04
because the class variable itself was never modified.This behavior is a common source of confusion for beginners in Python OOP. The general rule of thumb is:
Employee.raise_amount
) or an instance (emp_1.raise_amount
).Employee.raise_amount = 1.05
).emp_1.raise_amount = 1.10
), which will then shadow the class variable for that instance.Understanding the distinction between instance and class variables, and particularly the rules of attribute lookup and modification, is fundamental to writing correct and predictable object-oriented code in Python.
Site by
Ryan McBride