Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~26 min· topic 2 of 12

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.

main.pywhole filepython
class Dog:
    species = "Canis familiaris"      # class attribute: shared
    count = 0

    def __init__(self, name):
        self.name = name              # instance attribute: one per object
        Dog.count += 1

02How 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.

How Python finds an attributediagram
Rendering diagram…

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.

main.pywhole filepython
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.

main.pywhole filepython
print(vars(rex))          # {'name': 'Rex'}
print("species" in vars(Dog))   # True

07When 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. 1

    Predict, then run

    In the first example, write down the output of the last three print lines 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. 2

    Change the class value

    After the last line add Dog.species = "Wolf" and print bella.species and rex.species. Both change, because neither has its own copy. Now set bella.species = "Mine" first, then change Dog.species again: Bella does not move.

  3. 3

    Fix the shared list

    In the second example move items = [] out of the class body and into an __init__ method of Basket. Run it and check that a.items and b.items are now different lists.

Code & diagrams

Lookup order and hiding New tab

Predict each printed line before you run. Then change Dog.species at the end and see which dogs change.

Sign in to run this example in your browser.

Expected output

Canis familiaris Canis familiaris
Dogs made: 2
['name']
False
Rex's own
Canis familiaris
Canis familiaris
['name', 'species']
Canis familiaris
The shared list bug and the += trap New tab

Both baskets show both fruits. Move the list into __init__ in Basket and watch the bug disappear.

Sign in to run this example in your browser.

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']
Where each attribute livesdiagram
Rendering diagram…

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.

main.pywhole filepython
class Dog:
    count = 0

    def __init__(self):
        self.count += 1


Dog()
Dog()
print(Dog.count)
terminal
$ python main.py
── what you'll see ──
0

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__.

main.pywhole filepython
class Dog:
    def __init__(self, name):
        self.name = name


print(Dog.name)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 6, in <module>
print(Dog.name)
^^^^^^^^
AttributeError: type object 'Dog' has no attribute '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.name calls type(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.speak finds the function on the class and wraps it as a bound method that remembers rex. 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. Use field(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.ClassVar in annotated classes (Phase 10) to tell tools that an attribute is shared on purpose.

Remember this

  1. 1

    An instance attribute is set on self, usually in __init__: self.name = name. (Recall from Topic 8.1: self is 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. 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. 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 for rex only. To change the shared value for everyone, assign on the class: Dog.species = "x".

  4. 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. 5

    The augmented assignment self.total += 1 is a second trap. It means self.total = self.total + 1: Python reads total from the class (0), adds one, and then assigns the result on the instance. The class value stays 0 and the instance gets its own total. For a number that every object should count together, write Class.total += 1 explicitly.

  6. 6

    Every object keeps its attributes in a dictionary called __dict__, and vars(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

01

What is the difference between an instance attribute and a class attribute?

02

In what order does Python look for obj.attr, and what does that mean for assignment?

03

Why is items = [] in a class body usually a bug?

04

Why does self.count += 1 not update a class-level counter?

Practice

01

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.

02

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.

03

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.** Writing Dog.species makes 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.attr and 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.