CodeRigor

FREE LESSON

Classes, instances & owned state

Define real Python classes with per-instance attributes, bound methods and explicit ownership of mutable state.

A class gives a name to a useful combination of state and behavior. Calling LabelledShelf("front") creates an instance; its __init__ method establishes that instance’s starting attributes. The class object and each instance are different objects. Two shelves can share the same method definitions while keeping different names and different item lists.

Start with a responsibility: a shelf remembers a name and an ordered sequence of item labels. A dictionary can hold data, but a class lets the public operations and the rules about that data live together. A class is useful here because we will add items over time and want every observation to follow one interface. A one-off calculation with no continuing state can remain an ordinary function.

Shared definition, independent state
start / 2
Both shelves use one class definition; each shelf owns a separate mutable item list.
Shared definition, independent state
Both shelves use one class definition; each shelf owns a separate mutable item list.

Calling an instance method

self is the conventional first parameter of an instance method. Python binds an instance when you access its method: front.add("map") passes front as self. In this example, Shelf.add(front, "map") is the equivalent explicit call. The parameter name is a convention, while the binding behavior comes from accessing the function through an instance.

Python
class Shelf:
    kind = "shelf"

    def __init__(self, name, items=()):
        self.name = name
        self._items = list(items)

    def add(self, item):
        self._items.append(item)

    def snapshot(self):
        return tuple(self._items)

front = Shelf("front", ["map"])
back = Shelf("back")
front.add("pen")
Shelf.add(back, "ticket")
print(front.name, front.snapshot())
print(back.name, back.snapshot())
print(Shelf.kind)

The class attribute kind is an intentionally shared description. Assigning self.name and self._items creates instance attributes. A mutable list placed at class level would instead be shared by instances that look it up there. Mutating that shared list through one shelf could affect every other shelf. Creating a new list inside __init__ makes the intended ownership visible.

Decide what the constructor owns

Even an instance attribute can point to borrowed storage. Assigning self._items = items would keep the caller’s list, so later caller edits would change the shelf. list(items) copies the outer container. This task stores strings, so sharing those immutable elements is safe; a shallow copy would need more thought if the elements themselves were mutable entities.

A constructor copies the list
start / 2
Caller input and shelf storage start with equal contents but can change independently.
A constructor copies the list
Caller input and shelf storage start with equal contents but can change independently.

A tuple snapshot copies the current sequence of references into immutable observation data. Adding to the shelf later cannot add to an old tuple. This is a structural snapshot, not a general deep-freeze operation. The underscore in _items marks an internal detail by convention; it does not prevent another Python program from reaching that attribute.

Before the build, trace two empty shelves, two shelves initialized from the same list, and an old snapshot observed after an addition. Predict which values should change. The checks practice method binding and shared state; the bug hunts break ownership and instance naming; the fill establishes owned storage. Then implement LabelledShelf against the exact public build contract.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch the difference between class attributes and instance attributes, especially rebinding versus mutating a shared object. Use __init__ for each shelf’s list; class-level kind is intentionally shared.