DEV Community

Cover image for Class vs Instance Variables: The Mutation Trap That Catches Experienced Developers
Ameer Abdullah
Ameer Abdullah

Posted on

Class vs Instance Variables: The Mutation Trap That Catches Experienced Developers

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)
Enter fullscreen mode Exit fullscreen mode

What do you think this prints?

Output:

[90, 85]
[90, 85]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

[90]
[85]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

0
2
1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)