PYTHON / OBJECT-ORIENTED PYTHON
Class attributes versus instance attributes
Predict and control whether an attribute lives on the class or on one instance, and why assignment through an instance never touches the class.
What you will learn
- Read an attribute lookup as: instance __dict__ first, then the class
- Know that obj.x = v always writes to the instance, shadowing the class value
- Spot the shared-mutable-default bug and fix it by assigning in __init__
- Use vars(obj) or obj.__dict__ to prove where a value actually lives
Understanding Class attributes versus instance attributes
A class attribute is stored once, in the class object's namespace, and every instance can see it. An instance attribute lives in that one object's own namespace, its __dict__. When you write obj.x, Python looks in obj.__dict__ first and only falls back to type(obj) if there is nothing there. That single lookup rule explains almost every surprise in this topic.
Writing is not symmetric with reading. obj.x = 5 never modifies the class; it inserts the key 'x' into obj.__dict__, which then shadows any class attribute of the same name for that object only. So a class attribute works well as a shared default: instances that never assign to it keep seeing the class value, and if you later change the class value, those instances see the new one immediately, because the lookup happens fresh on every access.
The trap is the difference between rebinding a name and mutating an object. self.items = [] is an assignment, so it creates a per-instance list. self.items.append(1) is not an assignment: it reads self.items, finds the one list sitting on the class, and mutates it, so every instance sees the change. Mutable class attributes are therefore fine for genuinely shared state and wrong for per-object data.
class Counter:
total = 0 # class attribute: one copy, on the class
def __init__(self, name):
self.name = name # instance attribute: one copy per object
Counter.total += 1
a = Counter("a")
b = Counter("b")
print(Counter.total, a.total, b.total)
print(a.__dict__)
a.total = 99 # writes into a's own namespace, shadowing the class
print(Counter.total, a.total, b.total)
print(a.__dict__)
del a.total # remove the shadow; lookup falls back to the class
print(a.total)Attribute reads fall back from the instance to the class, but attribute writes always land on the instance.
Worked examples
Shared list versus per-instance list
Shows why a mutable class attribute leaks data between instances and how assigning in __init__ fixes it.
class Team:
members = [] # one list, shared by all instances
def __init__(self, label):
self.label = label
def add(self, person):
self.members.append(person) # mutation, not assignment
class FixedTeam:
def __init__(self, label):
self.label = label
self.members = [] # fresh list per instance
def add(self, person):
self.members.append(person)
x, y = Team("x"), Team("y")
x.add("ana")
y.add("bo")
print(x.members, y.members, Team.members)
print("members" in x.__dict__)
p, q = FixedTeam("p"), FixedTeam("q")
p.add("ana")
q.add("bo")
print(p.members, q.members)Example explained
Line 1self.members.append(person) performs no assignment, so it resolves self.members to the class list and mutates it.
Line 2x.members, y.members and Team.members print the same contents because they are three names for one list object.
Line 3FixedTeam assigns self.members = [] inside __init__, so each call to FixedTeam() builds a separate list.
Line 4The fix is not about append; it is about making sure each object owns its own list before appending.
A class attribute as a live default
Demonstrates that instances without their own value track later changes to the class value.
class Request:
timeout = 5.0 # default shared by every request
def __init__(self, url):
self.url = url
r1 = Request("/a")
r2 = Request("/b")
r2.timeout = 0.5 # only r2 gets its own value
print(r1.timeout, r2.timeout)
Request.timeout = 30.0 # change the default afterwards
print(r1.timeout, r2.timeout)
print(vars(r2))Example explained
Line 1r2.timeout = 0.5 adds 'timeout' to r2's namespace; Request.timeout is untouched.
Line 2r1 never assigned timeout, so r1.timeout is resolved on the class every time it is read and becomes 30.0.
Line 3r2 keeps 0.5 because its own namespace is searched first and wins.
Line 4vars(r2) shows exactly two keys, confirming which values are instance-owned.
Important notes
Reading an attribute never adds anything to the instance namespace; only assignment does, which is why the shadowing appears only after a write.
del obj.x removes an instance attribute; if the name only exists on the class, del obj.x raises AttributeError rather than deleting the class attribute.
Common mistakes
Writing self.total += 1 to bump a class-level counter: the read finds the class value, the write creates an instance attribute, so the class counter stays frozen and each object drifts on its own.
Declaring tags = [] in the class body as a per-object default: every instance appends into the same list, so one object's data shows up in all the others.
Setting obj.limit = 10 expecting to change the default for all instances: only that object is affected, and the class default silently keeps its old value for everyone else.
Try it yourself
Change, predict, then run
Write a class Robot with a class attribute count = 0 and an instance attribute serial set in __init__, incrementing the count through the class. Create three robots, then print Robot.count, each robot's serial, and vars() of one robot to confirm count is not in the instance namespace.
Open the Python workspaceCheck your understanding
A class declares tags = [] in its body. Inside a method, self.tags.append('x') affects every instance, but self.tags = self.tags + ['x'] affects only one. Why?
- append mutates the single list stored on the class, while the assignment binds a brand-new list into that instance's namespace
- append is defined on the class, so it always operates on class-level data, while + is defined on instances
- Both change the class list; the difference is that + is slower and copies the data
- Reading self.tags copies the class list into the instance, so append works on a copy while + works on the original
Show answer
append performs no assignment, so self.tags resolves to the one list on the class and mutates it in place; the second form evaluates a new list and then assigns it, and any assignment through self writes into the instance namespace, leaving the class list untouched. The last option is tempting but wrong: reading an attribute never copies anything into the instance, which is exactly why append can reach the shared list at all.