Class attributes and shared state: follow the lookup path
Trace Python class-attribute lookup, instance shadowing and shared mutable state, then compare Java static and instance qualification. Objects and access · Object-oriented programming.
An event app prints a badge for every visitor. Each badge starts with the label "new" and a shared list of tags. Staff then mark one visitor's badge "priority" and add the tag "urgent". Why does only that badge say "priority", while every badge shows "urgent"?
Where each value lives
Here is the badge class in Python. label and tags are written directly in the class body, so they belong to the class itself: each is a class attribute. owner is set through self in __init__, so every badge gets its own: an instance attribute.
class Badge:
label = "new"
tags = []
def __init__(self, owner):
self.owner = owner
first = Badge("A")
second = Badge("B")The class body runs once, so there is one label value and one list, called L1 here and in the animation. Neither badge has its own label or tags yet. When Python reads first.label, it checks first first; finding nothing, it checks the class and finds Badge.label. The value is not copied into the badges; each read goes back to the class. Attributes here are plain: ordinary stored values such as strings and lists, not properties or other special attributes.
Now first.label = "priority" runs. Assignment writes on the object before the dot, so first gets its own label; Badge.label stays "new" and second is untouched. Reading first.label now stops at first and finds "priority", while second.label still goes on to the class and finds "new". We say the instance attribute shadows the class attribute: it hides it for that one badge without changing it.
first.label = "priority"
print(first.label) # priority
print(second.label) # new
print(Badge.label) # newMutation and rebinding ask different questions
Now the list. Neither badge has its own tags, so a read of first.tags or second.tags goes through to the one list L1. append edits that list in place, so both badges and Badge.tags show the change, not only first.
first.tags.append("urgent")
print(second.tags) # ['urgent']
print(Badge.tags) # ['urgent']
first.tags = ["private"]
print(first.tags) # ['private']
print(second.tags) # ['urgent']The append call changes the list object L1 that the class attribute names. No tags attribute has been created on either instance at that point.
The final assignment has a different shape. first.tags = ["private"] binds a new list, L2, to first.tags. It does not mutate L1 and does not change Badge.tags, so second.tags continues to read the shared L1. This is the same identity-versus-rebinding boundary from the classes lesson, now combined with Python's instance-first lookup.
Badge: instance first, then class
Read the steps as text
- Badge stores label and tags (list L1). first stores owner. second stores owner. Nothing from the class is copied into the instances.
- first.label finds no label on first, so the lookup moves on to Badge and finds "new".
- first.label = "priority" gives first its own label, "priority". Badge still holds "new", and second still reads it.
- first.label stops at first: first has its own label, "priority".
- second.label finds no label on second, so the lookup moves on to Badge and finds "new".
- first.tags.append("urgent") reaches list L1 on Badge and adds "urgent", so L1 holds 1 item. Everything that reaches L1 sees it: first and second.
- A new list, L2, is built holding "private". Nothing refers to it yet. first.tags = ["private"] gives first its own tags, list L2. Badge still holds list L1, and second still reads it.
- second.tags finds no tags on second, so the lookup moves on to Badge and finds list L1, which holds "urgent".
Replacing the class's list is not mutating it
One more case: the class itself is given a different list. Keep a second name for the old one first, so you can see which list each step touches.
saved = Badge.tags # saved names L1, the old class list
Badge.tags = ["global"] # the class now names a new list, L3
saved.append("archived") # changes L1 only
print(first.tags) # ['private']
print(second.tags) # ['global']
print(saved) # ['urgent', 'archived']Assigning through Badge changes which list the class names; it does not touch any badge's own attributes. first already has its own tags (L2), so it keeps it. second has none, so its read falls through to the class and now finds L3. saved still names L1, so the later append changes L1 only and reaches neither badge.
Replacing the class default
Read the steps as text
- Badge stores tags (list L1). first stores owner and tags (list L2). second stores owner. Nothing from the class is copied into the instances.
- saved = Badge.tags makes saved refer to list L1, the list Badge.tags already refers to.
- A new list, L3, is built holding "global". Nothing refers to it yet. Badge.tags = ["global"] changes Badge.tags from list L1 to list L3. second now reads list L3. first keeps its own tags. saved still holds list L1.
- saved.append("archived") reaches list L1 through the name saved and adds "archived", so L1 holds 2 items. first and second do not reach L1, so nothing else changes.
- first.tags stops at first: first has its own tags, list L2, which holds "private".
- second.tags finds no tags on second, so the lookup moves on to Badge and finds list L3, which holds "global".
Java uses a keyword instead of a lookup rule
A static field belongs to the class itself: there is one copy however many objects exist. Visit.total names it; number has no static, so every object has its own copy and a.number names the one inside a. In the constructor, this.number makes the receiver explicit. Inside a static method there is no object, so this does not exist and an unqualified instance field such as number is rejected by the compiler.
final class Visit {
static int total = 0;
int number;
Visit(int number) {
this.number = number;
Visit.total += 1;
}
}
Visit a = new Visit(101);
Visit b = new Visit(102);
System.out.println(Visit.total); // 2
System.out.println(a.number); // 101class Visit:
total = 0
def __init__(self, number):
self.number = number
Visit.total += 1
a = Visit(101)
b = Visit(102)
print(Visit.total) # 2
print(a.number) # 101Java: one static slot, one slot per object
Read the steps as text
- Visit holds the static field total = 0: one slot for the whole class, however many objects exist. None exist yet.
- a = new Visit(101) makes object a with its own number slot. this.number = 101 changes a's own number slot: 0 to 101. Visit.total += 1 changes the one total slot on Visit: 0 to 1.
- b = new Visit(102) makes object b with its own number slot. this.number = 102 changes b's own number slot: 0 to 102. Visit.total += 1 changes the one total slot on Visit: 1 to 2.
- Visit.total reads the one total slot on Visit: 2.
- a.number reads the number slot inside a: 101.
- a.total = 9 is written through a, but total is static: it changes the one slot on Visit, 2 to 9, so a and b both read 9.
- b.total is not stored in b: total is static, so it reads the class's one slot, 9.
Python can express the same shared counter with a class attribute, but the rules differ. In Java the declaration decides: even a.total = 9, written through an object, changes the one Visit.total that b also sees (tools often warn, so write Visit.total). In Python the same-looking a.total = 9 would give a its own total and leave Visit.total alone.
This lesson stops at fields and lookup. Python @classmethod and @staticmethod receiver behavior belongs to a separate method-binding lesson, so do not infer those rules from the field comparison.
What belongs on the class
Put a value on the class only when every instance should share it. A string such as label = "new" is a safe default: the only way an instance can change it is assignment, which shadows instead of editing. A list is different, because append edits the shared object itself. If each badge needs its own tags, create the list in the constructor:
final class Badge {
final java.util.List<String> tags = new java.util.ArrayList<>(); // one list per Badge
final String owner;
Badge(String owner) { this.owner = owner; }
}class Badge:
label = "new"
def __init__(self, owner):
self.owner = owner
self.tags = [] # a fresh list for every badgeNow first.tags.append("urgent") changes only first's list, and second.tags stays empty. Keep tags = [] on the class only when sharing one list across every badge is the intent.
Recap
- A Python read checks the instance first and then the class; the class value is never copied into instances.
- Assigning
first.labelgivesfirstits own attribute that shadows the class value;secondandBadge.labelare unchanged. appendon a list reached through the class changes the one shared list, so every instance without its owntagsshows it; replacingBadge.tagschanges only which list the class names, not instances that have their own or an old alias.- Java
staticis one copy per class, instance fields one per object; put per-object data in the constructor, shared data on the class.
Go deeper
The trace uses ordinary class attributes and deliberately leaves out descriptors, properties and metaclasses. Those features can customize attribute lookup, but they do not change the distinction this lesson is isolating: an instance assignment can create an instance-owned binding, while a method such as append can mutate an object reached through a class-owned binding.
This lookup path has two stops, instance then class. Inheritance and construction opens the next module: when a class has a parent, the child is built on the parent's setup and a read continues up through the parent's names. When you need to decide which object should own a shared part, see object relationships. Practise telling a changed link from a mutated node in Linked List Fundamentals.