What is the difference between class attributes and instance attributes in Python, and why are mutable class attributes dangerous?
Class attributes are defined on the class object and shared by all instances; instance attributes are defined on the individual instance and shadow any class attribute of the same name. A mutable class attribute (such as a list or dict) is shared across all instances, so mutating it via one instance mutates it for every other instance — a common and silent bug.
How to think about it
The question is really testing whether you understand Python’s attribute lookup chain. Python checks instance.__dict__ first, then the class, then parent classes up the MRO. Class attributes live on the class object itself — shared memory — until an instance assignment shadows them with something local.
That distinction is harmless for immutables like int or str (you can only rebind, not mutate). It becomes a real trap the moment the class attribute is a list or dict.
How attribute lookup works
Assigning a.max_retries = 10 doesn’t change the class attribute — it creates a brand-new entry in a.__dict__ that shadows it. Calling a.results.append(...) is completely different: Python finds results on the class (because the instance has no such entry), then calls .append() on that shared object in place. No new entry is created, no shadowing happens.
The call sites look identical. That’s what makes this bug so sneaky.
Immutable class attribute — benign sharing
class Config:
max_retries: int = 3 # class attribute
a = Config()
b = Config()
a.max_retries = 10 # creates a NEW instance attribute on `a`; class attribute unchanged
print(a.max_retries) # 10 — instance attribute
print(b.max_retries) # 3 — still reading the class attribute
print(Config.max_retries) # 3
Assigning to a.max_retries creates an instance attribute that shadows the class attribute. b and the class itself are unaffected.
Mutable class attribute — shared and dangerous
Run this playground to see the bug live:
Why the fix works
self.results = [] inside __init__ writes into the instance’s own __dict__ the moment the object is created. From that point on, self.results resolves to the instance attribute, not the class one. Every instance gets its own fresh list.
An even cleaner pattern for Python 3.7+ is @dataclass with field(default_factory=list), which enforces this automatically and makes the intent explicit in the class signature.