Instance vs Class Variables
OOP Core
Instance vs Class Variables
Python classes hold both shared and per-instance state. Know where your data lives to avoid cross-instance side effects.
Comparison
| Type | Defined | Accessed | Use for |
|---|---|---|---|
| Class variable | Inside class body | Class.attr or instance.attr | Defaults, configuration, counters |
| Instance variable | Inside methods (self.attr) | instance.attr | Per-object state |
class Cart:
tax_rate = 0.07 # class variable — one copy, shared by every cart
def __init__(self):
self.items = [] # instance variable — a fresh list per cart
The difference shows immediately with two instances:
c1 = Cart()
c2 = Cart()
c1.items.append('book')
print(c1.items) # -> ['book']
print(c2.items) # -> [] — each cart has its own list
print(c1.tax_rate, c2.tax_rate) # -> 0.07 0.07 — both read the same shared value
How attribute lookup works
Reading instance.attr checks the instance's own namespace first, then falls back to the class. That is why every cart "has" a tax_rate it never stored itself — and why changing the class attribute is visible everywhere at once:
class Cart:
tax_rate = 0.07
c = Cart()
print(c.tax_rate) # -> 0.07 — not on the instance, found on the class
Cart.tax_rate = 0.09 # update the shared value on the class
print(c.tax_rate) # -> 0.09 — every existing instance sees it
Assignment through an instance does something different: it creates a new instance attribute that shadows the class one, for that object only:
class Cart:
tax_rate = 0.07
c1 = Cart()
c2 = Cart()
c1.tax_rate = 0.2 # creates an instance attribute on c1 only
print(c1.tax_rate) # -> 0.2 — shadows the class attribute
print(c2.tax_rate) # -> 0.07
print(Cart.tax_rate) # -> 0.07 — the class attribute never changed
del c1.tax_rate # remove the shadow
print(c1.tax_rate) # -> 0.07 — the class attribute is visible again
Rule of thumb: reads fall through to the class; writes through self or an instance stay on the instance.
Mutable class attributes
Mutable objects at class level are shared across all instances:
class Problem:
values = [] # shared across all instances
p1 = Problem()
p2 = Problem()
p1.values.append(1)
print(p2.values) # [1] — p2 sees p1's change!
Why doesn't shadowing save you here? Because p1.values.append(1) never assigns to p1.values — it reads the attribute (falling back to the class) and then mutates the one shared list in place. No new instance attribute is created. The same trap exists with mutable default arguments in __init__.
Fix by initializing in __init__:
class Fixed:
def __init__(self):
self.values = [] # each instance gets its own list
f1 = Fixed()
f2 = Fixed()
f1.values.append(1)
print(f2.values) # -> [] — independent
When class attributes are right
Shared state is a feature when sharing is the point:
- Constants and configuration —
tax_rate, a defaultcurrency, an API base URL. - Defaults that instances may override — immutable values like numbers and strings are safe, since assignment shadows instead of mutating.
- Counters and registries — state that genuinely belongs to the class as a whole.
class Sensor:
unit = 'C' # shared default
count = 0 # shared counter
def __init__(self, name):
self.name = name
Sensor.count += 1 # update through the class, not through self
s1 = Sensor('temp')
s2 = Sensor('humidity')
print(Sensor.count) # -> 2
Note the counter is incremented as Sensor.count += 1. Writing self.count += 1 would read the class value but write an instance attribute — every sensor would report count == 1.
Seeing where attributes live
__dict__ shows each namespace directly, which makes the whole model concrete:
class Cart:
tax_rate = 0.07
def __init__(self):
self.items = []
c = Cart()
print(c.__dict__) # -> {'items': []}
print('tax_rate' in c.__dict__) # -> False — not stored on the instance
print('tax_rate' in Cart.__dict__) # -> True — lives on the class
c.tax_rate = 0.1
print(c.__dict__) # -> {'items': [], 'tax_rate': 0.1}
When an attribute behaves strangely, print both instance.__dict__ and Class.__dict__ — the answer is almost always which namespace the name landed in. This layout matters again when inheritance adds more classes to the lookup chain.
Frequently Asked Questions
Why do all my objects share the same list?
The list was defined in the class body, so there is exactly one list object attached to the class, and every instance reads that same list through attribute lookup. Appending mutates it in place for everyone. Move the assignment into __init__ so each instance builds its own list.
class Fixed:
def __init__(self):
self.values = [] # one list per instance
How do I give each instance its own default value?
Assign it to self inside __init__. For immutable defaults (numbers, strings, tuples) a class attribute is also fine, because assigning a new value through the instance shadows rather than mutates. For mutable defaults (lists, dicts, sets), always create them in __init__.
Does assigning through an instance change the class attribute?
No. Assignment like c.tax_rate = 0.2 creates an instance attribute that shadows the class attribute for that one object; the class value and all other instances are untouched. To change the shared value, assign on the class: Cart.tax_rate = 0.2.
c1.tax_rate = 0.2 # only c1
Cart.tax_rate = 0.2 # every cart without a shadow