Class vs Instance Variables: The Mutation Trap That Catches Experienced Developers
Shared mutable class attributes produce non-obvious behavior that appears regularly in senior-level Python interviews
class Student:
grades = []
def add_grade(self, grade):
self.grades.append(grade)
alice = Student()
bob = Student()
alice.add_grade(90)
bob.add_grade(85)
print(alice.grades)
print(bob.grades)
What do you think this prints?
Output:
[90, 85]
[90, 85]
Both students share the same grades list. This is the class variable mutation trap.
Why This Happens
Class variables are defined on the class object itself, not on instances. When you access self.grades, Python first checks the instance's __dict__. Finding nothing, it looks up the attribute in the class. Both alice and bob find the same list object on the Student class.
.append() mutates that shared list object. All instances see the change.
The Fix
class Student:
def __init__(self):
self.grades = [] # instance variable, created fresh for each instance
def add_grade(self, grade):
self.grades.append(grade)
alice = Student()
bob = Student()
alice.add_grade(90)
bob.add_grade(85)
print(alice.grades)
print(bob.grades)
Output:
[90]
[85]
Moving the list initialization to __init__ creates a new list for each instance.
Instance Variable Shadowing
class Counter:
count = 0
def increment(self):
self.count += 1
c1 = Counter()
c2 = Counter()
c1.increment()
c1.increment()
c2.increment()
print(Counter.count)
print(c1.count)
print(c2.count)
Output:
0
2
1
self.count += 1 is equivalent to self.count = self.count + 1. Reading self.count finds the class variable (0). The assignment creates a new instance variable on self that shadows the class variable. The class variable remains 0 throughout.
This is different from the mutable class attribute case. Integers are immutable, so += creates a new object and binds it to the instance, rather than mutating the class-level object.
When Class Variables Are Appropriate
Class variables are appropriate for:
Constants shared across all instances
Configuration values that apply to all instances
Counters that genuinely track class-wide state (with careful management)
class DatabaseConnection:
MAX_CONNECTIONS = 10 # constant — appropriate as class variable
active_connections = 0 # class-wide counter — appropriate if managed carefully
def __init__(self):
if DatabaseConnection.active_connections >= DatabaseConnection.MAX_CONNECTIONS:
raise RuntimeError("Connection limit reached")
DatabaseConnection.active_connections += 1
Note that modifying a class variable should be done through the class name, not through self, to make the intent explicit and avoid accidental instance variable creation.
PyCodeIt 2.0 is officially here!
I’ve rebuilt your ultimate Python interview sandbox from the ground up. Whether you are a computer science student, a data science graduate, or a developer preparing to ace technical interviews, Python code tracing is a highly tested yet rarely practiced skill. PyCodeIt is built specifically to bridge that gap, and our latest update changes everything.
Practice class variable tracing problems at pycodeit.com.
Top comments (0)