Python Part I · Module 16 · Object-Oriented Programming
Instance and Class Attributes
An attribute can live in two places: on the individual object, or on the class that all the objects share. The syntax for defining the second is simpler than for the first, which is unfortunate, because it is almost never what you want — and the way it fails is not a crash but a quiet divergence, where one object's value changes and the others keep the old one. All of it follows from a single lookup rule that takes one sentence to state and explains every surprise in this chapter.
Intermediate16 min readPythonPart IFundamentalsOOPClasses
Part I · Module 16 · Chapter 16.4 · Instance and Class Attributes
1. Two Places a Name Can Live
class TestRun:
default_timeout = 3600 # on the CLASS -- one copy, shared
def __init__(self, name):
self.name = name # on the INSTANCE -- one copy each
a = TestRun("smoke")
b = TestRun("cdc")
print("a.name:", a.name, "| b.name:", b.name)
print("a.default_timeout:", a.default_timeout)
print("b.default_timeout:", b.default_timeout)
print("TestRun.default_timeout:", TestRun.default_timeout)
print("a.__dict__:", a.__dict__)a.name: smoke | b.name: cdc
a.default_timeout: 3600
b.default_timeout: 3600
TestRun.default_timeout: 3600
a.__dict__: {'name': 'smoke'}Look at the last line. default_timeout is not in the instance's storage at
all, yet a.default_timeout returned 3600. That is the lookup rule at work.
2. The Lookup Rule
Reading
obj.xchecks the instance first. If there is noxthere, it checks the class.
That is the whole mechanism. Everything else in this chapter follows from it.
class TestRun:
default_timeout = 3600
def __init__(self, name):
self.name = name
a = TestRun("smoke")
print("in instance storage :", "default_timeout" in a.__dict__)
print("in class storage :", "default_timeout" in TestRun.__dict__)
print("reachable from a :", a.default_timeout)
print("name in instance :", "name" in a.__dict__)
print("name in class :", "name" in TestRun.__dict__)in instance storage : False
in class storage : True
reachable from a : 3600
name in instance : True
name in class : FalseTwo separate storages, one lookup that walks from the first to the second.
3. Writing Does Not Follow the Same Path
Reading falls through to the class. Writing never does.
class TestRun:
default_timeout = 3600
def __init__(self, name):
self.name = name
a = TestRun("smoke")
b = TestRun("cdc")
a.default_timeout = 60 # looks like an update; is not
print("a:", a.default_timeout)
print("b:", b.default_timeout)
print("class:", TestRun.default_timeout)
print("a.__dict__:", a.__dict__)
print("b.__dict__:", b.__dict__)a: 60
b: 3600
class: 3600
a.__dict__: {'name': 'smoke', 'default_timeout': 60}
b.__dict__: {'name': 'cdc'}a.default_timeout = 60 created a new instance attribute that shadows the
class one. b and the class are untouched — and a.__dict__ now has a key it
did not have before, which is the visible evidence.
The failure mode this produces is subtle:
class TestRun:
default_timeout = 3600
def __init__(self, name):
self.name = name
a = TestRun("smoke")
b = TestRun("cdc")
a.default_timeout = 60 # shadowed
TestRun.default_timeout = 7200 # policy change, later
print("a (shadowed):", a.default_timeout)
print("b (not) :", b.default_timeout)a (shadowed): 60
b (not) : 7200A later change to the class default reached b and could not reach a,
because a has its own copy now. In a flow where the class attribute is a
configurable default, that is an object silently exempt from the configuration.
4. Updating a Class Attribute Properly
Assign through the class, not through an instance:
class TestRun:
default_timeout = 3600
def __init__(self, name):
self.name = name
a = TestRun("smoke")
b = TestRun("cdc")
TestRun.default_timeout = 7200
print("a:", a.default_timeout)
print("b:", b.default_timeout)
print("neither has its own copy:", a.__dict__, b.__dict__)a: 7200
b: 7200
neither has its own copy: {'name': 'smoke'} {'name': 'cdc'}Both instances see the new value, because neither has a shadowing entry and the lookup falls through.
5. The Expensive Mistake: a Mutable Class Attribute
Everything above involved an int, where shadowing is at least visible. With a
list, the sharing is invisible until it hurts:
class Parser:
errors = [] # ONE list, shared by every instance ever
def __init__(self, name):
self.name = name
def record(self, message):
self.errors.append(message)
a = Parser("smoke")
b = Parser("cdc")
a.record("bad handshake")
print("a.errors:", a.errors)
print("b.errors:", b.errors)
print("Parser.errors:", Parser.errors)
print("same list:", a.errors is b.errors)
print("a.__dict__:", a.__dict__)a.errors: ['bad handshake']
b.errors: ['bad handshake']
Parser.errors: ['bad handshake']
same list: True
a.__dict__: {'name': 'smoke'}b reports an error it never had. One parser recorded a failure and every
parser in the program now claims it.
Note why Section 3's shadowing did not save you here: self.errors.append(...)
is a read followed by a mutation, not a write. The read falls through to the
class, finds the shared list, and appends to it. No instance attribute is ever
created — a.__dict__ still holds only name.
This is Chapter 11.5's mutable-default trap in a new costume, and it is worse, because the shared value now looks like per-object state.
The fix is one line:
class Parser:
def __init__(self, name):
self.name = name
self.errors = [] # a NEW list per instance
def record(self, message):
self.errors.append(message)
a = Parser("smoke")
b = Parser("cdc")
a.record("bad handshake")
print("a.errors:", a.errors)
print("b.errors:", b.errors)
print("same list:", a.errors is b.errors)a.errors: ['bad handshake']
b.errors: []
same list: FalseAnything mutable belongs in
__init__, never in the class body.
6. The One Case Where Sharing Is the Point
Counting instances genuinely requires shared state, and it must be written through the class:
class TestRun:
created = 0
def __init__(self, name):
self.name = name
TestRun.created += 1 # through the CLASS
for name in ["smoke", "cdc", "burst"]:
TestRun(name)
print("instances created:", TestRun.created)
r = TestRun("arb")
print("after one more :", TestRun.created, "| r sees:", r.created)
print("r has own copy :", "created" in r.__dict__)instances created: 3
after one more : 4 | r sees: 4
r has own copy : FalseTestRun.created += 1 is deliberate. Writing self.created += 1 would read the
class value, add one, and assign the result to the instance — Section 3's
shadowing — leaving the class counter at zero forever and giving every object
its own useless 1.
7. Which to Use
The canonical purpose asks which of the two you almost always want, and the answer is not balanced:
| Use a class attribute for | Use an instance attribute for |
|---|---|
| a constant shared by all instances | everything else |
a default that is immutable (int, str, bool, None) | all per-object data |
| a counter updated through the class name | anything mutable, always |
In practice: if it varies per object, or is a list, dict or set, it goes in
__init__. A class attribute is for a genuine constant — a timeout default, a
version string, a set of valid status names that nobody mutates.
8. Predict, Run, Observe
class Job:
retries = 0
tags = []
def __init__(self, name):
self.name = name
a = Job("smoke")
b = Job("cdc")
a.retries += 1
a.tags.append("nightly")
print("A:", a.retries, b.retries, Job.retries)
print("B:", a.tags, b.tags, Job.tags)
print("C:", a.__dict__)
print("D:", a.tags is b.tags)
print("E:", "retries" in a.__dict__, "tags" in a.__dict__)A: 1 0 0
B: ['nightly'] ['nightly'] ['nightly']
C: {'name': 'smoke', 'retries': 1}
D: True
E: True FalseA and B behave in opposite ways from two lines that look alike.
a.retries += 1 is a write — read 0 from the class, add one, assign to the
instance. It shadowed, so b and the class stay at 0.
a.tags.append(...) is a read then a mutation — the read fell through to the
class and the append modified the shared list in place. Nothing was assigned, so
no shadow was created, and all three names see the change.
C and E are the proof: retries is now in the instance's storage and tags
is not.
D confirms a.tags and b.tags are the same object.
The distinction to carry away: = shadows, .append() shares. Which one a
line performs is not always obvious, and += on a list does the second.
9. Exercises
Exercise 1 — two storages. Define a class with one class attribute and one
instance attribute. Show with __dict__ where each lives.
Exercise 2 — the fall-through. Read a class attribute from an instance and confirm it is absent from the instance's storage.
Exercise 3 — shadowing. Assign a class attribute through one instance. Show the other instance and the class are unaffected, and point to the new key.
Exercise 4 — the exempt object. Shadow a value on one instance, then change the class attribute. Explain why one object did not get the update.
Exercise 5 — the shared list. Reproduce Section 5's bug. Prove the lists are the same object, then fix it and prove they are not.
Exercise 6 — read versus write. For self.x += 1 where x is a class
attribute, say whether a shadow is created. Verify it.
Exercise 7 — the counter. Write an instance counter using the class name.
Then write the broken version with self and explain the result.
Exercise 8 — classify. For each, choose class or instance attribute: (a) a test's seed (b) the default timeout (c) the list of errors found (d) the set of legal status strings (e) the number of jobs submitted so far.
10. Summary
- An attribute can live on the instance or on the class; the class body defines the second.
- Reading checks the instance first, then the class. That one rule explains everything here.
- Writing through an instance never updates the class — it creates a shadowing instance attribute.
- A shadowed object is exempt from later changes to the class attribute.
- Update a class attribute through the class name.
- A mutable class attribute is shared by every instance, and mutating it creates no shadow, so the change is visible everywhere.
=shadows;.append()shares — and+=on a list does the second.- Anything mutable belongs in
__init__. - Class attributes are for constants, immutable defaults, and counters updated through the class.
- If it varies per object, it is an instance attribute — which is almost always.
The next chapter gives these objects behaviour as well as data.
Where this fits
Part of the Python curriculum.
