Module 5 · Object-Oriented Programming ⏱ 19 min

Instance vs. Class Attributes

By the end of this lesson you will be able to:
  • Define a class attribute shared by every instance of a class
  • Read and set instance attributes that belong to one object only
  • Explain how Python looks up a name on the instance first, then the class
  • Avoid the shared mutable default trap with class attributes
  • Choose correctly between class and instance attributes for a given design

Every object-oriented program faces the same design question: what belongs to the blueprint, and what belongs to each building made from it? A class attribute is defined inside the class body and shared by every instance. An instance attribute is attached to self inside __init__ and belongs to one object alone.

Getting this wrong is not a style issue — it is a bug factory. Put a list on the class and every instance shares the same list. Put a constant on every instance and you waste memory and break the ability to update every object at once. The distinction is the foundation of clean object design: shared constants and defaults live on the class; unique state lives on the instance.

In this lesson you will learn the lookup rules that govern attribute access, the classic mutable-default trap, and how to inspect exactly where each name lives.

flowchart TD
  C["class Dog\nspecies = ... (shared)"] --> A["rex.name = 'Rex'"]
  C --> B["bo.name = 'Bo'"]
  style C fill:#3776ab,color:#fff
A class attribute lives on the class; instance attributes live on each object.

The anatomy of a class attribute

A class attribute lives in the class body, at the same indentation level as method definitions. It is evaluated once, when the class statement runs, and stored in the class object itself.

class Dog:
    species = 'Canis familiaris'   # class attribute
    def __init__(self, name):
        self.name = name           # instance attribute

Both dogs read the same species, but each keeps its own name. rex.name is found directly on the instance; rex.species is found by walking up to the class. That walk happens automatically, but understanding it is what separates confident OOP from guesswork.

Class attributes are shared; instance attributes are per-object.
class Dog:
    species = 'Canis familiaris'
    def __init__(self, name):
        self.name = name

rex = Dog('Rex')
bo = Dog('Bo')

print(rex.species, '|', bo.species)
Dog.species = 'C. lupus'
print(rex.species, '|', bo.species)
rex.species = 'mystery'
print(rex.species, '|', bo.species)

Lookup order: instance first, class second

When you read rex.species, Python checks the instance dictionary first. If the name is missing, it climbs to the class and checks there. If it is still missing, it continues up through parent classes. This chain is what makes inheritance work.

Change the class attribute and every instance that has not shadowed it sees the new value:

Dog.species = 'C. lupus'
print(rex.species)   # C. lupus

But assign on a single instance and you create an instance attribute that shadows the class attribute for that object only. Once an instance has its own copy, changing the class attribute no longer affects it, because Python stops at the instance layer. Reading is cooperative; writing is local. This asymmetry surprises beginners but protects objects from accidentally modifying shared state.

flowchart TD
  I["rex.species"] -->|"rex.__dict__?"| A{"has species?"}
  A -->|"no"| C["Dog.__dict__?"]
  C -->|"yes"| R["return Dog.species"]
  A -->|"yes (after assign)"| R2["return rex.species"]
  style I fill:#3776ab,color:#fff
  style R2 fill:#b45309,color:#fff
Python searches the instance first, then the class, then parent classes.

Why reading is safe but writing is local

A function may read an outer variable, but assigning it creates a local. Class attributes behave the same way. When rex.species is on the right side of an assignment, Python only reads — it walks the chain happily. When rex.species = 'mystery' appears, Python creates a new entry in rex's dictionary without ever touching the class.

This is why the mutable-default trap is so insidious. rex.tricks.append('roll over') is not an assignment — it is a read followed by a method call on the shared object. The class attribute is found, the list is modified in place, and every other instance sees the change. The instance dictionary remains empty, so the shadow never forms.

A class-level counter tracks how many instances have been created.
class Dog:
    count = 0
    def __init__(self, name):
        self.name = name
        Dog.count += 1

a = Dog('Rex')
b = Dog('Bo')
c = Dog('Mia')
print('Dogs created:', Dog.count)

Choosing between class and instance attributes

Use a class attribute for data that is true for every instance: a default value, a counter tracking how many objects have been created, or a constant like a tax rate. If changing the value for one object should change it for all of them, it belongs on the class.

Use an instance attribute for data that varies per object: a user's name, an account balance, or a sensor reading. When in doubt, default to instance attributes — they are safer because they cannot leak state between objects.

The real power comes from combining both. A class attribute provides a sensible default; an instance attribute can override it for special cases. This pattern is used throughout Python's standard library.

flowchart TD
  C["class Dog\nspecies = 'C. lupus'"] --> B["bo.species → class 'C. lupus'"]
  C -.->|"shadowed"| R["rex.species = 'mystery'\n(instance only)"]
  style C fill:#3776ab,color:#fff
  style R fill:#b45309,color:#fff
Assigning on an instance creates a shadow copy; the class attribute remains unchanged.
Inspect __dict__ to see exactly where each attribute lives.
class Dog:
    species = 'Canis familiaris'
    def __init__(self, name):
        self.name = name

rex = Dog('Rex')
print('Instance dict:', rex.__dict__)
print('Class has species:', 'species' in Dog.__dict__)
print('rex borrows species:', 'species' not in rex.__dict__)
rex.species = 'mystery'
print('After shadowing:', rex.__dict__)
Exercise

Define a Car class. Give it a class attribute wheels = 4 (shared by every car) and store make and model as instance attributes in __init__.

class Car:
    # class attribute shared by ALL cars

    def __init__(self, make, model):
        # store make and model as instance attributes
        pass
Exercise

What does this print? species is shared; each name is its own.

class Dog:
    species = 'Canis familiaris'
    def __init__(self, name):
        self.name = name

a = Dog('Rex')
b = Dog('Bo')
print(a.species)
print(b.name)
Exercise

Write a Counter class with a class attribute total = 0. Each time a Counter instance is created, increment Counter.total by 1. Do NOT use self.total.

class Counter:
    total = 0
    def __init__(self):
        # increment the CLASS total by 1
        pass
Exercise

This Team class is supposed to give every team its own list of players, but all teams share the same list. Fix it by moving the list into __init__.

class Team:
    players = []
    def add(self, name):
        self.players.append(name)
Exercise

Write a BankAccount class. It should have a class attribute interest_rate = 0.02 (shared by all accounts). Each instance should store balance as an instance attribute. Add a method apply_interest() that increases self.balance by self.balance * BankAccount.interest_rate.

class BankAccount:
    # class attribute for shared interest rate

    def __init__(self, balance):
        # instance attribute
        pass

    def apply_interest(self):
        # update balance using the class interest rate
        pass
Exercise

Write a Person class with a class attribute planet = 'Earth'. Add a method relocate(new_planet) that sets an instance attribute planet, shadowing the class default. Then write a function count_on_earth(people) that takes a list of Person instances and returns how many still use the class default (i.e., do not have their own planet instance attribute).

class Person:
    planet = 'Earth'

    def __init__(self, name):
        self.name = name

    def relocate(self, new_planet):
        # set instance attribute that shadows the class default
        pass

def count_on_earth(people):
    # count people whose planet still comes from the class
    pass

Recap

  • A class attribute lives on the class and is shared by every instance that has not shadowed it.
  • An instance attribute lives on self and belongs to one object only.
  • Python looks up names on the instance first, then the class — so an instance attribute shadows the class attribute.
  • Mutable class attributes (lists, dicts) are shared and dangerous because in-place mutation is a read, not an assignment.
  • Use class attributes for universal constants and defaults; use instance attributes for per-object state.
  • Inspect __dict__ to see exactly where each name lives.
  • When in doubt, prefer instance attributes — they are safer and easier to reason about.

Next you will learn about nested data structures and how to navigate containers inside containers.

Checkpoint quiz

Which kind of attribute is shared by every instance of a class?

If you run rex.species = 'mystery' on one dog, what happens to the other dogs?

Go deeper — technical resources