Topic 8.2
Instance vs Class Attributes
In one line
Data can live on each object (instance attributes) or once on the class (class attributes). When you read obj.name, Python looks on the object first, then on its class, which explains shared defaults, the shared-list bug and why assigning through an instance never changes the class.
Think of it like this
A school. Each student has their own name and roll number: that is per-student data. The school has one name and one address that every student shares: that is per-school data. If the school changes its name, every student's answer changes at once. If one student changes their name, nobody else's does. Per-student data is an instance attribute; per-school data is a class attribute.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Instance attribute
- Data stored on one object, usually set with
self.x = ...in__init__. - Class attribute
- Data stored once on the class and shared by all its objects.
- Lookup order
- The order Python searches for an attribute: the object first, then its class, then the class's parents.
- Shadowing (hiding)
- When an instance attribute has the same name as a class attribute, the instance one is found first and hides the class one.
- `__dict__`
- The dictionary where an object keeps its attributes.
vars(obj)shows it. - Mutable
- A value that can be changed in place, such as a list or dictionary.
- Constant
- A value that should never change. By convention it is written in UPPER_CASE.
Step by step
01Two places data can live
The class body can contain plain assignments. species and count below are class attributes. name is set through self, so it is an instance attribute.
Read access looks the same for both: rex.name and rex.species. The difference is where the value is stored.
class Dog:
species = "Canis familiaris" # class attribute: shared
count = 0
def __init__(self, name):
self.name = name # instance attribute: one per object
Dog.count += 102How Python finds an attribute
For rex.species, Python first checks the object's own dictionary. It is not there, so it checks the class, finds it and returns it. For rex.name, it is found at the first step.
The picture below shows the search. Each arrow is one step, and the search stops at the first match.
03Assigning always writes to the object
rex.species = "Rex's own" does not change the class. It adds species to rex's own dictionary. From now on rex.species finds that first and hides the class value. Bella and Dog.species are unchanged.
Deleting the instance attribute with del rex.species removes the hiding, and the class value shows through again.
04The shared mutable list
Here is the bug. items = [] in the class body creates one list. self.items.append(thing) looks up items, finds that one list on the class and appends to it. Both baskets share it, and vars(a) is empty because nothing was assigned on a.
The fix is to create the list in __init__, so every object gets its own.
class Basket:
items = [] # one list for every Basket (bug)
class SafeBasket:
def __init__(self):
self.items = [] # one list per object (correct)05The += trap with numbers
Numbers cannot be changed in place, so self.total += 1 becomes a read followed by an assignment. The read finds the class value. The assignment writes to the instance. The two do not meet, so each object counts alone and the class value never moves.
If you want one shared counter, write Score.total += 1, naming the class so the assignment goes to the class.
06Use vars() to see the truth
When an attribute behaves unexpectedly, print vars(obj) and vars(type(obj)). If the name is in the first, it belongs to the object. If only in the second, it is shared. That answers nearly every question in this topic.
print(vars(rex)) # {'name': 'Rex'}
print("species" in vars(Dog)) # True07When class attributes are the right tool
Use class attributes for constants that belong to the kind of thing (MAX_SPEED = 120), for default values you will not mutate, and for counters that truly count across all objects. Name constants in UPPER_CASE so readers know not to change them.
Do not use them as a default home for data that each object should own.
Try it yourself
- 1
Predict, then run
In the first example, write down the output of the last three
printlines before you run. Then run it and compare. Any line you got wrong tells you which part of the lookup order to study again. - 2
Change the class value
After the last line add
Dog.species = "Wolf"and printbella.speciesandrex.species. Both change, because neither has its own copy. Now setbella.species = "Mine"first, then changeDog.speciesagain: Bella does not move. - 3
Fix the shared list
In the second example move
items = []out of the class body and into an__init__method ofBasket. Run it and check thata.itemsandb.itemsare now different lists.
Code & diagrams
Predict each printed line before you run. Then change Dog.species at the end and see which dogs change.
Expected output
Canis familiaris Canis familiaris
Dogs made: 2
['name']
False
Rex's own
Canis familiaris
Canis familiaris
['name', 'species']
Canis familiarisBoth baskets show both fruits. Move the list into __init__ in Basket and watch the bug disappear.
Expected output
a.items: ['apple', 'banana']
b.items: ['apple', 'banana']
same list? True
instance dict of a: {}
c.items: ['apple']
d.items: ['banana']
instance total: 2 class total: 0
['total']Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Count with += on a class attribute
Use self.count += 1 in __init__ and expect Dog.count to rise.
class Dog:
count = 0
def __init__(self):
self.count += 1
Dog()
Dog()
print(Dog.count)Break #2
Ask for an attribute that only exists on an instance
Read Dog.name on the class, where name is set only in __init__.
class Dog:
def __init__(self, name):
self.name = name
print(Dog.name)Myth vs fact
Myth
Setting obj.x = 5 changes the class attribute x for every object.
Fact
Assignment through an instance always creates or updates an instance attribute. Only Class.x = 5 changes the shared one.
Myth
A list written in the class body is a fresh list for every object.
Fact
The class body runs once, so there is one list, shared by all objects. Create per-object lists in __init__.
Myth
Class attributes are the same as global variables.
Fact
They live in the class's namespace, not the module's. You reach them through the class or an instance (Dog.count, rex.count), and instances can hide them.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Attribute lookup on
obj.namecallstype(obj).__getattribute__. It first checks the class for a data descriptor (such as a property, Topic 8.4), then the instance__dict__, then the class for plain values and methods. That is why a property cannot be hidden by an instance attribute but a plain class attribute can. - ▸
Functions defined in a class are class attributes too. Looking up
rex.speakfinds the function on the class and wraps it as a bound method that remembersrex. This is the same lookup as any other class attribute. - ▸
dataclasses(Topic 8.8) treat class-level annotations as per-instance fields and reject plain mutable defaults, precisely because of the shared list bug. Usefield(default_factory=list)there. - ▸
Class attributes make good cheap constants, but a module-level constant is often clearer for values not tied to one class. Use
typing.ClassVarin annotated classes (Phase 10) to tell tools that an attribute is shared on purpose.
Remember this
- 1
An instance attribute is set on
self, usually in__init__:self.name = name. (Recall from Topic 8.1:selfis the object a method was called on.) Each object has its own copy. A class attribute is written directly in the class body, outside any method:species = "Canis familiaris". There is only one copy, stored on the class, and every instance can read it. - 2
When you read
rex.species, Python follows a fixed lookup order: first the object's own__dict__, then the class, then the class's parents (Topic 8.5). The first match wins. So an instance attribute hides a class attribute of the same name, without changing it. - 3
Assignment does not follow the lookup order.
rex.species = "x"always writes to the object, never to the class. It creates a new instance attribute that hides the class one forrexonly. To change the shared value for everyone, assign on the class:Dog.species = "x". - 4
This leads to the most famous Python object bug. If a class attribute is a mutable value, such as a list, then
self.items.append(x)does not assign anything. It finds the one shared list on the class and changes it in place, so every object sees the change. Always create mutable attributes inside__init__. - 5
The augmented assignment
self.total += 1is a second trap. It meansself.total = self.total + 1: Python readstotalfrom the class (0), adds one, and then assigns the result on the instance. The class value stays 0 and the instance gets its owntotal. For a number that every object should count together, writeClass.total += 1explicitly. - 6
Every object keeps its attributes in a dictionary called
__dict__, andvars(obj)returns it. It is the easiest way to see what really belongs to the object. Class attributes are not in the instance's__dict__; they are in the class's. Looking at both settles most confusion.
Explain it without notes
What is the difference between an instance attribute and a class attribute?
In what order does Python look for obj.attr, and what does that mean for assignment?
Why is items = [] in a class body usually a bug?
Why does self.count += 1 not update a class-level counter?
Practice
Write a class Student with a class attribute school = "Hectal High" and an instance attribute name. Create two students and print their schools, then change the school for all students in one line.
Write a class Ticket that counts how many tickets have been created, using a class attribute, and gives each ticket the next number as its number. Create three and print the numbers and the total.
The class below shares one list between all carts. Fix it so each cart has its own: class Cart: with items = [] and a method add(self, x) that appends.
Trade-offs
- ↔
Class attributes give cheap shared data and constants, but sharing is easy to do by accident. Anything mutable belongs in
__init__. Keep class attributes for values that really are the same for every object. - ↔
**Reading through the instance (
rex.species) is convenient but hides where the value lives.** WritingDog.speciesmakes the sharing visible to the reader at the cost of a little repetition.
Done when you can
Done when you can say where an instance attribute and a class attribute are stored.
Done when you can list the lookup order for
obj.attrand use it to predict output.Done when you can explain why assigning through an instance never changes the class.
Done when you can spot and fix the shared mutable class attribute bug.
Done when you can use
vars()to see which attributes belong to an object.