JEPA4Japan · tutorials

The Python Data Model

1,285 words 6 min read #Python

Implement special methods for representation, comparison, hashing, containers, and iteration.

Course progress Course outline 24 of 24 lessons available

Python syntax sends protocol requests

Special methods are not magic decorations. They are hooks with exact contracts. Normal syntax asks an object for a behavior.

  1. len(box) requests __len__
  2. a == b negotiates with __eq__
  3. for item in box requests an iterator
  4. box[0] requests __getitem__
Use public syntax; Python calls the matching protocol and checks its result.

Implement only protocols your domain can honestly support. For example, __len__ must return a non-negative integer—not the text "2".

Give values two faces and fair comparisons

repr(value) is a stable diagnostic face for developers. str(value) is a readable face for users. If __str__ is missing, Python falls back to __repr__.

class ReadingItem:
    def __init__(self, title, minutes):
        self.title = title
        self.minutes = minutes

    def __repr__(self):
        return f"ReadingItem({self.title!r}, {self.minutes!r})"

    def __str__(self):
        return f"{self.title} ({self.minutes} min)"

!r keeps quote boundaries visible. Never place secrets in repr; representations reach logs and tracebacks.

Equality is a negotiation. For an unsupported type, return the singleton NotImplemented, not False and not the exception NotImplementedError:

def __eq__(self, other):
    if type(other) is not type(self):
        return NotImplemented
    return (self.title, self.minutes) == (other.title, other.minutes)
  1. Ask left can you compare this type?
  2. NotImplemented let Python ask the other side
  3. Final result equality may be False; order may error
Unsupported equality usually ends as False; unsupported ordering normally raises TypeError.

Define < only when the domain has one meaningful order. A reading priority of (minutes, title) can be honest; a phone number “less than” another phone number usually is not. When contexts need different orders, callers should pass a sorting key.

For ordinary values, equality should remain reflexive, symmetric, and transitive. Choose its fields as part of the domain contract, not merely because they are available.

Hash only stable equality state

Hash containers require one law:

If a == b, then hash(a) == hash(b).

Hash exactly the stable fields and type used by equality:

def __hash__(self):
    return hash((type(self), self.title, self.minutes))

Those fields cannot change while the object is a dictionary key or set member. A class that defines value equality but remains mutable should be unhashable. When __eq__ is defined without __hash__, Python normally sets __hash__ = None; keep that safe default. Do not restore identity hashing beside field equality, because equal objects could then have different hashes.

Make a container answer without leaking its box

  1. len / bool size; zero is false
  2. in clear membership meaning
  3. iter a fresh iterator every time
  4. [0] and [1:] items and safe slices
A container answers common questions while its mutable storage stays internal.

If __bool__ is absent, Python uses __len__: zero is false, positive is true. __contains__ powers in. Each __iter__ call must return a new iterator so nested traversals do not share position. __getitem__ should support integer indexes, negative indexes, slices, and normal IndexError behavior. A slice may return a tuple or a new container, but never the internal mutable list itself.

Build a Pythonic reading queue

Save this complete standard-library project as reading_queue.py:

class ReadingItem:
    __slots__ = ("_title", "_minutes", "_sealed")

    def __init__(self, title, minutes):
        title = title.strip()
        if not title:
            raise ValueError("title must not be empty")
        if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
            raise ValueError("minutes must be a non-negative integer")
        object.__setattr__(self, "_title", title)
        object.__setattr__(self, "_minutes", minutes)
        object.__setattr__(self, "_sealed", True)

    def __setattr__(self, name, value):
        if getattr(self, "_sealed", False):
            raise AttributeError("ReadingItem is immutable")
        object.__setattr__(self, name, value)

    @property
    def title(self):
        return self._title

    @property
    def minutes(self):
        return self._minutes

    def __repr__(self):
        return f"ReadingItem({self.title!r}, {self.minutes!r})"

    def __str__(self):
        return f"{self.title} ({self.minutes} min)"

    def __eq__(self, other):
        if type(other) is not type(self):
            return NotImplemented
        return (self.title, self.minutes) == (other.title, other.minutes)

    def __lt__(self, other):
        if type(other) is not type(self):
            return NotImplemented
        return (self.minutes, self.title) < (other.minutes, other.title)

    def __hash__(self):
        return hash((type(self), self.title, self.minutes))


class ReadingQueue:
    __hash__ = None

    def __init__(self, items=()):
        self._items = []
        for item in items:
            self.add(item)

    def add(self, item):
        if not isinstance(item, ReadingItem):
            raise TypeError("item must be a ReadingItem")
        self._items.append(item)

    def __len__(self):
        return len(self._items)

    def __contains__(self, item):
        return item in self._items

    def __iter__(self):
        return iter(tuple(self._items))

    def __getitem__(self, index):
        if isinstance(index, slice):
            return tuple(self._items[index])
        return self._items[index]


def main():
    python = ReadingItem(" Python data model ", 30)
    same = ReadingItem("Python data model", 30)
    async_item = ReadingItem("Async I/O", 45)

    assert repr(python) == "ReadingItem('Python data model', 30)"
    assert python == same and hash(python) == hash(same)
    assert len({python, same}) == 1
    assert python != ("Python data model", 30)
    assert sorted([async_item, python]) == [python, async_item]

    queue = ReadingQueue([python, async_item])
    left, right = iter(queue), iter(queue)
    assert next(left) == next(right) == python
    assert len(queue) == 2 and bool(queue) and same in queue
    assert queue[-1] == async_item and queue[1:] == (async_item,)
    try:
        hash(queue)
    except TypeError:
        pass
    else:
        raise AssertionError("mutable queue was hashable")

    print("Tests passed.")
    print(f"Queue: {len(queue)} items")
    for item in queue:
        print(f"- {item}")


if __name__ == "__main__":
    main()

Output:

Tests passed.
Queue: 2 items
- Python data model (30 min)
- Async I/O (45 min)

The queue deliberately has no hash, iteration uses a fresh snapshot iterator, and slicing returns a tuple rather than its private list.

Three tiny missions and the handoff

  1. Two faces. Build Bookmark(title, page) with a diagnostic repr and reader-facing str; test a title containing quotes.
  2. Comparison handshake. Make Duration.__eq__ and __lt__ return NotImplemented for integers. Verify Duration(5) == 5 is false while Duration(5) < 5 raises TypeError.
  3. Independent shelf. Support length, membership, two simultaneous iterators, negative indexing, and tuple slices over private storage.

Sharp corners: direct special-method calls bypass public intent, NotImplementedError is not the comparison sentinel, invented ordering lies about the domain, mutable hash state breaks lookup, and returning storage leaks mutation.

  • I can map public syntax to protocol requests.
  • I can separate developer repr from reader str.
  • I return NotImplemented for unsupported comparison types.
  • I order only a domain with a real ordering rule.
  • I preserve the equality/hash law or leave mutable values unhashable.
  • I implement size, truth, membership, fresh iteration, indexing, and slicing.
  • My queue passes every assertion and exposes no storage list.
  • I completed the three tiny missions.

Next, you will separate concurrency from parallelism and choose threads or processes for the work each can perform safely.