Instance vs Class Attributes
An instance attribute belongs to one specific object (like each worker's name badge). A class attribute is shared by all objects of that type (like the warehouse address on the wall). This lesson teaches you the difference and shows you a common trap with shared lists.
Python's class attributes are stored on the class object itself and shared via the MRO lookup chain. The mutable class attribute trap (e.g., tags: list = [] at class level) is a classic Python gotcha that even experienced developers encounter. This lesson covers it explicitly.
In Lesson 3, James added methods to his Note class: .summarize(), .add_tag(), .is_long(). Every method used self to access the note's own data. Now he creates two notes and tries something:
note_a = Note("Python Tips", "Learn the basics of Python programming")
note_b = Note("Cooking Pasta", "Boil water and add salt")
note_a.title = "Advanced Python"
print(note_a.title)
print(note_b.title)
Output:
Advanced Python
Cooking Pasta
"Good," James says. "Changing one title did not touch the other." He tries something else:
print(Note.default_author)
"Wait -- what is default_author? Where does it live? On the class or on the instance?"
Emma pulls up the class definition. "That depends on where you defined it."
Two Places to Store Data
A class can store data in two places. The difference matters because it determines whether each object gets its own copy or all objects share the same value.
Instance Attributes: Unique Per Object
Instance attributes are created inside __init__ using self. Each object gets its own copy:
class Note:
def __init__(self, title: str, body: str) -> None:
self.title = title # instance attribute
self.body = body # instance attribute
self.tags: list[str] = [] # instance attribute
Create two notes. Modify one. Check the other:
note_a = Note("Python Tips", "Learn the basics")
note_b = Note("Cooking Pasta", "Boil water")
note_a.title = "Advanced Python"
print(note_a.title)
print(note_b.title)
Output:
Advanced Python
Cooking Pasta
Each note has its own title. Changing note_a.title does not affect note_b.
Class Attributes: Shared Across All Instances
Class attributes are defined directly in the class body, outside any method. Every instance reads the same value:
class Note:
default_author: str = "Anonymous" # class attribute
max_title_length: int = 100 # class attribute
def __init__(self, title: str, body: str) -> None:
self.title = title
self.body = body
Access the class attribute from any instance or from the class itself:
note_a = Note("Python Tips", "Learn the basics")
note_b = Note("Cooking Pasta", "Boil water")
print(note_a.default_author)
print(note_b.default_author)
print(Note.default_author)
Output:
Anonymous
Anonymous
Anonymous
All three print the same value. The attribute lives on the class, not on any individual note.
When to Use Each
| Use case | Attribute type | Example |
|---|---|---|
| Data unique to each object | Instance (self.x in __init__) | self.title, self.body, self.tags |
| Configuration shared by all objects | Class (in class body) | default_author, max_title_length |
| Constants that never change per instance | Class | supported_formats = ["md", "txt"] |
| Counters tracking all instances | Class | note_count = 0 |
The Mutable Class Attribute Trap
This is the bug that bites every Python programmer at least once. It looks correct. Pyright does not flag it. The tests pass until you create a second instance.
class Note:
tags: list[str] = [] # class attribute -- SHARED list
def __init__(self, title: str, body: str) -> None:
self.title = title
self.body = body
def add_tag(self, tag: str) -> None:
self.tags.append(tag)
Create two notes. Add a tag to one:
note_a = Note("Python Tips", "Learn the basics")
note_b = Note("Cooking Pasta", "Boil water")
note_a.add_tag("python")
print(note_a.tags)
print(note_b.tags)
Output:
['python']
['python']
Both notes have the tag. Adding a tag to note_a changed note_b because they share the same list object. The tags = [] in the class body creates one list. Every instance points to that same list.
The Fix: Create the List in __init__
Move the mutable attribute into __init__ so each instance gets its own list:
class Note:
default_author: str = "Anonymous" # immutable class attribute -- safe
def __init__(self, title: str, body: str) -> None:
self.title = title
self.body = body
self.tags: list[str] = [] # instance attribute -- each note gets its own list
Now test it:
note_a = Note("Python Tips", "Learn the basics")
note_b = Note("Cooking Pasta", "Boil water")
note_a.add_tag("python")
print(note_a.tags)
print(note_b.tags)
Output:
['python']
[]
Each note has its own tag list. The fix is one line: move tags: list[str] = [] from the class body into __init__ with self..
The Rule
| Attribute type | Safe as class attribute? | Why |
|---|---|---|
str, int, float, bool | Yes | Immutable. Reassignment creates a new value on the instance. |
list, dict, set | No | Mutable. All instances modify the same object. |
If the default value is mutable (a list, dict, or set), define it in __init__ with self.
PRIMM-AI+ Practice
Predict
Read this code. Before running it, write down what you think each print statement will output:
class Warehouse:
location: str = "Building A"
inventory: list[str] = []
def __init__(self, name: str) -> None:
self.name = name
def add_item(self, item: str) -> None:
self.inventory.append(item)
w1 = Warehouse("East Wing")
w2 = Warehouse("West Wing")
w1.add_item("Laptop")
w1.location = "Building B"
print(w1.inventory)
print(w2.inventory)
print(w1.location)
print(w2.location)
Write your predictions with a confidence score (1 to 5). Then switch to Plan Mode in Claude Code (press Shift+Tab) and type:
I predicted that w2.inventory is [] and w2.location is "Building A".
Am I right? Explain why inventory is shared but location is not.
Why Plan Mode? The AI explains without running code for you. You verify the explanation against your prediction before running anything.
Run
Run the code. Compare each line of output against your prediction.
Investigate
If your predictions were wrong, type /investigate in Claude Code:
/investigate
I expected w2.inventory to be [] but it was ['Laptop'].
Why does appending to w1.inventory also change w2.inventory,
but reassigning w1.location does NOT change w2.location?
The key insight: append mutates the existing list object (shared). Assignment with = creates a new object on the instance (not shared). This is why mutable class attributes are dangerous but immutable ones are safe.
Modify
Fix the Warehouse class so each warehouse has its own inventory. Run the code again and verify that w2.inventory is now [].
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Design an Attribute Layout
I am building a Note class for a note-taking app. Here are the
attributes I need:
- title (unique per note)
- body (unique per note)
- tags (list, unique per note)
- default_author (same for all notes unless overridden)
- max_title_length (same for all notes)
- created_notes_count (tracks how many notes have been created total)
For each one, should it be a class attribute or an instance
attribute? Explain your reasoning.
What you're learning: You are applying the class-vs-instance decision to a real design. The AI's reasoning reinforces the rule: shared configuration goes on the class, per-object data goes in __init__.
Prompt 2: Find the Bug
Here is a class with a subtle bug:
class TaskBoard:
columns: list[str] = ["To Do", "In Progress", "Done"]
tasks: dict[str, list[str]] = {}
def __init__(self, name: str) -> None:
self.name = name
def add_task(self, column: str, task: str) -> None:
if column not in self.tasks:
self.tasks[column] = []
self.tasks[column].append(task)
Create two TaskBoard instances. Add a task to one. Does the other
board show the task? Explain why, and show the fix.
What you're learning: You are diagnosing the mutable class attribute trap in unfamiliar code. The tasks dict is shared. The columns list is also shared but appears safe because nothing mutates it (yet). Spotting both is the skill.
Prompt 3: Write a Test
Write a pytest test that PROVES the mutable class attribute bug
exists. The test should:
1. Create two instances of a class with a shared list
2. Modify the list through one instance
3. Assert that the other instance is also affected
4. Then show the fixed version where the test passes correctly
Name the test test_mutable_class_attribute_bug.
What you're learning: You are encoding the bug as a test. Writing a test that proves a bug exists is a debugging technique from Chapter 56. Here you apply it to a design-level bug, not a logic error.
James leans back and stares at the two columns he drew on the whiteboard: "Shared" and "Per-Object."
"It is like the warehouse," he says. "The warehouse address is shared by all workers. You do not give each worker their own copy of the address. But each worker has their own name badge. If you put the inventory clipboard on the wall" -- he taps the "Shared" column -- "every worker writes on the same clipboard. Morning shift adds items. Night shift sees them. That is the mutable class attribute."
"And the fix?" Emma asks.
"Give each worker their own clipboard in their locker. That is self.inventory = [] in __init__." He draws a line from "Shared" to "Per-Object."
Emma nods. "The rule is simple: if it is mutable, put it in __init__. Strings, numbers, booleans are safe as class attributes because reassignment creates a new value. Lists, dicts, sets are not safe because methods like .append() modify the same object."
She pauses. "I still get bitten by this about once a year. It is subtle enough that even experienced developers miss it. The code looks correct, pyright does not flag it, and the bug only appears when you create a second instance. Your first test with one instance passes. Your second test with two instances fails. That is why the 'two instances' test is a standard check for any class you write."
James writes on the whiteboard: "Two instances. Modify one. Check the other."
"That is your class-level smoke test," Emma says. "You have instance attributes, class attributes, and the mutable trap. Now you have all the pieces. Time to put them together."