datarekha
Python Easy Asked at GoogleAsked at AmazonAsked at MetaAsked at MicrosoftAsked at Airbnb

What is the difference between class attributes and instance attributes in Python, and why are mutable class attributes dangerous?

The short answer

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 probing Python’s attribute-lookup chain. Reading instance.attr, Python checks the instance’s own __dict__ first, then the class, then up the MRO. A class attribute lives on the class object itself — shared by every instance — until an instance assignment shadows it with a local entry. That’s harmless for immutables like int or str, where you can only rebind. It turns into a real trap the moment the class attribute is a list or dict.

The sneaky part is that the mutation and the read look identical at the call site. a.max_retries = 10 creates a new entry in a.__dict__ that shadows the class attribute. But a.results.append(...) doesn’t assign anything — Python finds results on the class (the instance has no such entry), then calls .append() on that one shared object. No new entry, no shadowing, just everyone’s data changing at once.

A worked example

# DANGER: one list, defined on the class, shared by every instance
class BatchJob:
    results = []

job1 = BatchJob()
job2 = BatchJob()
job1.results.append("record_1")              # mutates the CLASS-level list
print("job1.results:", job1.results)
print("job2.results:", job2.results)         # also changed — surprise!
print("Same object?", job1.results is job2.results)

# The fix: create the list per-instance in __init__
class BatchJobFixed:
    def __init__(self):
        self.results = []                    # a fresh list for each instance

j1 = BatchJobFixed()
j2 = BatchJobFixed()
j1.results.append("record_1")
print("j1.results:", j1.results)
print("j2.results:", j2.results)             # independent
print("Same object?", j1.results is j2.results)
job1.results: ['record_1']
job2.results: ['record_1']
Same object? True
j1.results: ['record_1']
j2.results: []
Same object? False

The first block is the bug in three lines: job1 appended one record, and job2 — which never touched anything — now reports the same record, because job1.results is job2.results is literally True; they’re one list. The fix moves the list into __init__, so self.results = [] writes into each instance’s own __dict__ at construction time, and the two jobs end up with separate lists.

An even cleaner fix

For Python 3.7+, a @dataclass with field(default_factory=list) enforces the per-instance list automatically and states the intent right in the signature:

from dataclasses import dataclass, field

@dataclass
class BatchJob:
    results: list = field(default_factory=list)   # fresh list per instance
Learn it properly Classes & Instances

Keep practising

All Python questions

Explore further

Skip to content