C.W.K.
Stream
Lesson 02 of 07 · published

Instance vs Class Attributes — and the Mutable Default Trap (Again)

~18 min · instance-attribute, class-attribute, mutable-default

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Two kinds of attributes

An attribute defined inside a method (typically __init__) with self.x = ... is an instance attribute — each instance gets its own. An attribute defined at the class level (in the class body, not inside a method) is a class attribute — shared across all instances. Both are accessible via instance.attribute, which makes the distinction subtle.

Lookup order — instance first, then class

When you access obj.x, Python looks in the instance's __dict__ first. If not found, it checks the class. If still not found, it walks up the inheritance chain. This is why obj.x = value creates an instance attribute that shadows the class attribute — it doesn't modify the class.

The mutable class attribute trap

If you put a mutable object as a class attribute (history = [] at class level), every instance shares it. Calling instance.history.append(x) doesn't shadow — it mutates the shared list. This is the same kind of trap as the mutable default argument; the fix is to assign in __init__: self.history = [].

Warning: Class attributes are great for genuine constants and class-wide settings (RETRY_LIMIT = 3), and bad for anything mutable. The rule of thumb: if it's primitive and immutable, class-level is fine. If it's a list, dict, or set, it lives on the instance.

Class-level vs instance-level methods — the difference is __get__

Functions defined in a class body become methods because Python's descriptor protocol turns them into bound methods on access. This is why self appears automatically. Class attributes that aren't functions don't get this treatment — they come back as themselves.

Code

Instance vs class attribute lookup·python
class Robot:
    species = "robot"            # CLASS attribute — shared

    def __init__(self, name):
        self.name = name          # INSTANCE attribute — per instance

r1 = Robot("R1")
r2 = Robot("R2")

print(r1.species, r2.species)    # robot robot   — same value
print(r1.name, r2.name)          # R1 R2          — different

# Setting on the instance SHADOWS, doesn't modify the class
r1.species = "android"
print(r1.species, r2.species)    # android robot
print(Robot.species)             # robot          — class unchanged
Mutable class attribute — the trap·python
# DON'T DO THIS
class Bad:
    history = []                  # class attribute — SHARED across instances

    def __init__(self, name):
        self.name = name

    def log(self, event):
        self.history.append(event)

a = Bad("a")
b = Bad("b")
a.log("x")
b.log("y")
print(a.history)        # ['x', 'y']  <- a saw b's log!
print(b.history)        # ['x', 'y']
print(a.history is b.history)   # True — same list

# DO THIS — assign on the instance in __init__
class Good:
    def __init__(self, name):
        self.name = name
        self.history = []         # instance attribute — fresh list each time

    def log(self, event):
        self.history.append(event)

a = Good("a")
b = Good("b")
a.log("x")
b.log("y")
print(a.history)        # ['x']
print(b.history)        # ['y']
Class-level constants — fine and idiomatic·python
class Connection:
    DEFAULT_TIMEOUT = 30          # class attribute — shared CONSTANT
    MAX_RETRIES = 3

    def __init__(self, host):
        self.host = host

    def connect(self, timeout=None):
        if timeout is None:
            timeout = self.DEFAULT_TIMEOUT     # access via instance
        return f"connecting to {self.host} (timeout={timeout})"

c = Connection("localhost")
print(c.connect())               # uses 30 from class
print(c.connect(timeout=10))     # explicit override
print(Connection.MAX_RETRIES)    # access via class directly
Looking at the dicts — instance vs class·python
class Robot:
    species = "robot"

    def __init__(self, name):
        self.name = name

r = Robot("R1")

print(r.__dict__)              # {'name': 'R1'}            — instance attrs only
print("species" in r.__dict__) # False                     — not on instance
print("species" in Robot.__dict__)  # True                  — lives on class

# r.species works because Python falls back to the class
print(r.species)               # 'robot'

External links

Exercise

Build a class Quiz with: a class-level constant MAX_QUESTIONS = 10, an instance attribute answers (a list, initialized fresh in __init__), and a method add_answer(text) that appends to answers and raises RuntimeError if len(answers) would exceed MAX_QUESTIONS. Create two Quiz instances and confirm each has its own answers list — adding to one doesn't affect the other. Then deliberately try the trap: re-define Quiz with answers = [] at class level, repeat the test, and observe the bug.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.