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
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 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
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.
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
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__)
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
Put
wheels = 4directly inside the class body — noself.In init, assign self.make = make and self.model = model.
class Car:
wheels = 4
def __init__(self, make, model):
self.make = make
self.model = model
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)
species is shared — both dogs read the class attribute.
b.name is 'Bo', set on that instance in init.
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
Use
Counter.total += 1inside init.This touches the class attribute, not an instance one.
class Counter:
total = 0
def __init__(self):
Counter.total += 1
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)
Remove the class-level
players = [].Create
self.players = []inside init instead.
class Team:
def __init__(self):
self.players = []
def add(self, name):
self.players.append(name)
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
Use
BankAccount.interest_rateinside apply_interest.Change the class attribute and verify new accounts pick it up.
class BankAccount:
interest_rate = 0.02
def __init__(self, balance):
self.balance = balance
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rate
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
Use
self.planet = new_planetto create an instance attribute.Check
p.__dict__to see if the instance has its own planet attribute.
class Person:
planet = 'Earth'
def __init__(self, name):
self.name = name
def relocate(self, new_planet):
self.planet = new_planet
def count_on_earth(people):
count = 0
for p in people:
if 'planet' not in p.__dict__:
count += 1
return count
Recap
- A class attribute lives on the class and is shared by every instance that has not shadowed it.
- An instance attribute lives on
selfand 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.