CHAPTER 40 · ADVANCED4 min read

The Data Model and Dunder Methods

Make small domain objects cooperate with Python syntax through coherent, minimal protocols.

Python 3.12+Free to readModule 9 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Implement useful representation, iteration, and containment.
  • Respect equality and hashing invariants.
  • Return NotImplemented for unsupported binary operands.

01Syntax calls protocols

Python syntax delegates behavior to special methods on a type. len(value), iteration, indexing, arithmetic, and with statements each have a protocol. Implementing a small coherent protocol lets existing tools work with your object. It is usually more useful than adding many special methods to make an object imitate several unrelated built-in types at once.

Special methods are normally looked up on the class, so assigning an instance attribute named __len__ is not a reliable way to change len(instance). Design behavior on the type. Begin with a clear invariant: for a playlist, stored tracks preserve order and clients cannot mutate the private sequence accidentally. The tuple inside this example provides a stable snapshot of the input iterable.

playlist.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02Representations serve different readers

__repr__ should help a developer identify an object and investigate its state; __str__ can provide a friendly display. An evaluable repr is useful when practical, but clarity is more important than pretending every object can be reconstructed. Neither representation should disclose tokens, passwords, or sensitive payloads merely because the object appears in a log or an assertion failure.

A protocol carries expectations beyond the existence of a method. __len__ must return a nonnegative integer. __iter__ should return an iterator, and a container usually returns a fresh one for every call. Indexing should raise IndexError when an index is outside a finite sequence. Surprising contracts force every consumer to learn private exceptions and defeat the benefit of ordinary Python syntax.

03Equality and hashing must agree

If two objects compare equal, their hashes must match whenever they are hashable. Keys must not change the state used for equality while they live in a dictionary or set. A mutable value object is therefore usually unhashable. A frozen dataclass can provide value equality and a suitable generated hash when all of its participating fields are themselves hashable.

For binary operators, return NotImplemented when the operand type is unsupported. This gives Python an opportunity to try the reflected operation or report a useful TypeError. Raising NotImplementedError means something different: it is an exception commonly used for an operation deliberately left unimplemented. A money type would also need currency rules and rounding policy; a numeric wrapper alone cannot establish those domain decisions.

distance.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Implement the smallest honest interface

Protocols should simplify a real use case. A configuration object may need readable representation and key access, while a resource owner may need __enter__ and __exit__. It rarely needs arithmetic. Prefer composition with standard containers to a partial imitation of every list method. Collections abstract base classes can document a contract, but registering a type does not magically supply missing behavior.

Test the syntax that callers use, including unsupported operands and empty values. Verify that two iterations over a container are independent and that equal immutable values behave as one set member. Keep object construction invariants visible and validate them once. The result should make ordinary code easier to predict; clever syntax that conceals mutation or remote requests usually makes debugging harder.

LET IT CLICK

The ideas worth keeping.

  • Implement protocols around explicit domain invariants.
  • Hashable keys need stable equality-related state.
  • NotImplemented is a dispatch signal, not an exception.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

A reusable range container

Implement Countdown(top) for nonnegative integers. It must support len() and repeated independent iteration from top to 1. Reject a negative top.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
class Countdown:    def __init__(self, top):        if type(top) is not int or top < 0:            raise ValueError('top must be a nonnegative integer')        self.top = top    def __len__(self):        return self.top    def __iter__(self):        return iter(range(self.top, 0, -1))countdown = Countdown(3)assert len(countdown) == 3assert list(countdown) == list(countdown) == [3, 2, 1]

The container creates a fresh range iterator for every iteration. It does not keep a shared cursor on itself.

02

An immutable lookup key

Use a frozen dataclass for Route(origin, destination). Verify duplicate routes collapse in a set and reverse direction remains distinct.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from dataclasses import dataclass@dataclass(frozen=True)class Route:    origin: str    destination: strroutes = {Route('A', 'B'), Route('A', 'B'), Route('B', 'A')}assert len(routes) == 2assert Route('A', 'B') in routes

The generated equality uses ordered field values. Immutable string fields make the generated hash consistent with that equality.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What should __add__ return for an unsupported operand type?
02Two equal hashable objects must have which property?
03Why return iter(self._tracks) instead of storing one iterator?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

A reusable range container

Implement Countdown(top) for nonnegative integers. It must support len() and repeated independent iteration from top to 1. Reject a negative top.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.