Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.x checks the instance first. If there is no x there, it checks the class.

That is the whole mechanism. Everything else in this chapter follows from it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
in instance storage : False
in class storage    : True
reachable from a    : 3600
name in instance    : True
name in class       : False

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a (shadowed): 60
b (not)     : 7200

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a.errors: ['bad handshake']
b.errors: []
same list: False

Anything 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
instances created: 3
after one more   : 4 | r sees: 4
r has own copy   : False

TestRun.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 forUse an instance attribute for
a constant shared by all instanceseverything else
a default that is immutable (int, str, bool, None)all per-object data
a counter updated through the class nameanything 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1 0 0
B: ['nightly'] ['nightly'] ['nightly']
C: {'name': 'smoke', 'retries': 1}
D: True
E: True False

A 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.