The Python data model
Make your own classes work with print, ==, sorting, len, for loops and operators by implementing the special methods Python calls behind the scenes.
What you will learn
- Explain why Python calls special (dunder) methods and that you rarely call them yourself
- Write __repr__ and __str__ for readable output
- Implement __eq__ and __hash__ together, and say why defining one affects the other
- Make instances sortable with __lt__, and countable and iterable with __len__ and __iter__
- Support obj[i], in, if obj and obj() with __getitem__, __contains__, __bool__ and __call__
- Overload an operator such as + and return NotImplemented for unsupported operands
On this page
Why does len(songs) work on a list, a + b on two strings, and for x in data on almost anything? Because these built-in operations ask the object to do the work, by calling a method with a special name. Those names start and end with two underscores, which is why they are called dunder methods. The set of them, and the rules for using them, is the data model. Implement the right ones and your class feels like a built-in type.
You write the dunder methods but almost never call them directly. You write len(mix), and Python calls mix.__len__() for you.
Showing an object: __repr__ and __str__
When you print an instance of a plain class, you get something unhelpful, like <__main__.Point object at 0x7f...>. Define __repr__ to say what the object is. By convention it looks like the code that would build it:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __repr__(self): return f"Point({self.x}, {self.y})"
p = Point(1, 2)print(p)print([p, Point(3, 4)])print(repr(p))Point(1, 2)[Point(1, 2), Point(3, 4)]Point(1, 2)print uses __repr__ when the class defines nothing else, and so do lists when they display their contents. __repr__ is aimed at developers, and it shows up in tracebacks and debugger sessions.
When a friendlier text for end users is wanted, add __str__. print() and f-strings use __str__ first, and containers still use __repr__:
class Money: def __init__(self, cents): self.cents = cents
def __repr__(self): return f"Money({self.cents})"
def __str__(self): return f"${self.cents // 100}.{self.cents % 100:02d}"
price = Money(1999)print(price)print(repr(price))print([price])print(f"total: {price}")$19.99Money(1999)[Money(1999)]total: $19.99If you write only one, write __repr__. str() falls back to it when __str__ is missing.
Equality: __eq__
By default == on instances means “are these the very same object?”:
class Point: def __init__(self, x, y): self.x = x self.y = y
a = Point(1, 2)b = Point(1, 2)print(a == a)print(a == b)TrueFalseTwo points with the same coordinates are different objects, so they are unequal. Define __eq__ to compare by value instead:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y
a = Point(1, 2)b = Point(1, 2)print(a == b)print(a != Point(5, 5))print(a == (1, 2))TrueTrueFalseThe isinstance check handles the case where the other side is a different type. Returning NotImplemented, a special built-in value and not an exception, tells Python “I don’t know how to compare with that.” Python then tries the other operand’s __eq__, and if neither knows, falls back to identity, so a == (1, 2) is False. Python also derives != from __eq__, so you do not write it yourself.
Hashing: __hash__
Sets and dictionary keys need a hash, a whole number that is the same for equal objects. Here is a rule that surprises most learners: a class that defines __eq__ and not __hash__ becomes unhashable.
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y
print(Point.__hash__ is None)try: {Point(1, 2)}except TypeError as error: print("TypeError:", error)TrueTypeError: cannot use 'Point' as a set element (unhashable type: 'Point')Python removed the hash because the old one, based on identity, would now disagree with your new idea of equality. The longer wording of this message, which names the set, is new in Python 3.14 Requires version 3.14 or later. Earlier versions say only unhashable type: 'Point', and the behaviour is the same.
The rule to follow is that objects which compare equal must have equal hashes. The usual recipe hashes a tuple of the fields that __eq__ compares:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y
def __hash__(self): return hash((self.x, self.y))
points = {Point(1, 2), Point(1, 2), Point(3, 4)}print(len(points))labels = {Point(0, 0): "origin"}print(labels[Point(0, 0)])2originThe duplicate point vanished from the set, and a freshly built Point(0, 0) found the dictionary entry. This relies on the fields not changing after the object goes into a set or dictionary, because a changed field changes the hash and the entry becomes unreachable. Only define __hash__ on objects you treat as immutable. The dataclasses lesson shows an easy way to get that.
Ordering: __lt__
sorted, min and max only ever ask “is this one less than that one?”, which is __lt__:
class Version: def __init__(self, major, minor): self.major = major self.minor = minor
def __repr__(self): return f"Version({self.major}, {self.minor})"
def __lt__(self, other): if not isinstance(other, Version): return NotImplemented return (self.major, self.minor) < (other.major, other.minor)
versions = [Version(3, 12), Version(2, 7), Version(3, 9)]print(sorted(versions))print(min(versions), max(versions))print(Version(3, 9) > Version(3, 8))[Version(2, 7), Version(3, 9), Version(3, 12)]Version(2, 7) Version(3, 12)TrueComparing tuples does the field-by-field work. Notice that > worked without a __gt__ method: when a > b has no method, Python tries b < a instead. That reflection is why __lt__ alone is enough for sorting. If you need all six comparison operators, the standard library offers functools.total_ordering, and dataclasses can generate them with order=True.
Size and iteration: __len__ and __iter__
Define __len__ and your class works with len(). Define __iter__ and it works in for loops, in list(), and in other places that need to go through its items one by one:
class Playlist: def __init__(self, songs): self._songs = list(songs)
def __len__(self): return len(self._songs)
def __iter__(self): return iter(self._songs)
mix = Playlist(["Intro", "Waltz", "Finale"])print(len(mix))for song in mix: print(song)print("Waltz" in mix)print(list(mix))print(bool(mix), bool(Playlist([])))3IntroWaltzFinaleTrue['Intro', 'Waltz', 'Finale']True False__iter__ here hands back an iterator over the inner list, using the built-in iter. The next module explains iterators in full, and for now this one-line form is all you need. Three things came free. The in check worked, because Python falls back to looping through __iter__. list(mix) worked. And bool(Playlist([])) is False, because an object with __len__ is falsy when its length is zero, the same rule you learned for empty lists.
Indexing, membership, truth and calls
Four more dunder methods cover everyday syntax: x[i], in, if x: and x(). A small Shelf class and a Multiplier class show them one at a time.
Indexing: __getitem__
obj[key] calls obj.__getitem__(key). The key can be an integer, a negative integer or a slice, and passing it on to the inner list handles all three:
class Shelf: def __init__(self, books): self._books = list(books)
def __getitem__(self, index): return self._books[index]
shelf = Shelf(["Dune", "Emma", "Ulysses"])print(shelf[0])print(shelf[-1])print(shelf[0:2])DuneUlysses['Dune', 'Emma']shelf[0:2] reached __getitem__ as a slice object, and the list knew what to do with it. Python also has an older fallback: with __getitem__ and no __iter__, a for loop and the in operator try shelf[0], shelf[1] and so on until an IndexError ends the loop:
class Shelf: def __init__(self, books): self._books = list(books)
def __getitem__(self, index): return self._books[index]
shelf = Shelf(["Dune", "Emma", "Ulysses"])for book in shelf: print(book)print("Emma" in shelf)DuneEmmaUlyssesTrueFor a class that holds a sequence, __getitem__ alone is enough to be read like one. Add __len__ as well so that len(shelf) works.
Membership: __contains__
item in obj calls obj.__contains__(item) when it exists. Define it when the check should follow your own rule, here ignoring capitals:
class Shelf: def __init__(self, books): self._books = list(books)
def __contains__(self, title): return title.lower() in [book.lower() for book in self._books]
shelf = Shelf(["Dune", "Emma"])print("dune" in shelf)print("Hamlet" in shelf)print("Hamlet" not in shelf)TrueFalseTruenot in needs nothing extra: Python negates the answer of __contains__.
Truth: __bool__
if obj: and bool(obj) call obj.__bool__(), which must return True or False. Without it, Python uses __len__ (zero is falsy), and without that every instance is truthy:
class Wallet: def __init__(self, cents): self.cents = cents
def __bool__(self): return self.cents > 0
for wallet in (Wallet(250), Wallet(0)): if wallet: print("has money") else: print("empty")print(bool(Wallet(1)))has moneyemptyTrueCalling: __call__
obj(args) calls obj.__call__(args). An instance with __call__ behaves like a function that also remembers state:
class Multiplier: def __init__(self, factor): self.factor = factor
def __call__(self, value): return value * self.factor
triple = Multiplier(3)print(triple(14))print(callable(triple))print(list(map(triple, [1, 2, 3])))42True[3, 6, 9]callable(triple) is True, so triple can be passed anywhere a function is expected, such as map or the key= argument of sorted. The decorators lesson uses this to build decorators from classes.
Operators: __add__ and friends
Every operator maps to a dunder method: + to __add__, * to __mul__, - to __sub__, abs() to __abs__, and so on. Implement one and the operator works:
class Money: def __init__(self, cents): self.cents = cents
def __repr__(self): return f"Money({self.cents})"
def __add__(self, other): if not isinstance(other, Money): return NotImplemented return Money(self.cents + other.cents)
def __mul__(self, factor): if not isinstance(factor, int): return NotImplemented return Money(self.cents * factor)
print(Money(250) + Money(199))print(Money(250) * 3)Money(449)Money(750)Both methods check the other operand and return NotImplemented for anything unsupported. Python then raises a TypeError with a clear message, which is better than returning nonsense:
class Money: def __init__(self, cents): self.cents = cents
def __add__(self, other): if not isinstance(other, Money): return NotImplemented return Money(self.cents + other.cents)
try: Money(250) + 5except TypeError as error: print("TypeError:", error)TypeError: unsupported operand type(s) for +: 'Money' and 'int'Keep operators for cases where the meaning is obvious. Adding two Money values is natural. Giving + a creative meaning on a class of your own will confuse the next reader.
Common pitfalls
Defining __eq__ without __hash__. The instances can no longer go into sets or be dictionary keys, as shown above. Define both, or neither.
Hashing a mutable object. If a field that feeds the hash can change, a set containing the object can lose track of it. Make such objects immutable, or leave them unhashable.
A __repr__ that returns something other than a string. It must return str. Returning a number or None raises TypeError.
Raising instead of returning NotImplemented. In __eq__, __lt__ and the arithmetic methods, returning NotImplemented lets the other operand try. Raising TypeError yourself skips that chance.
Calling dunder methods directly. Write len(x), repr(x) and a + b. The direct form x.__len__() bypasses checks that the built-in functions make, and it reads strangely.
Write a class Vector(x, y) with these special methods: __repr__ giving Vector(3, 4); __eq__ comparing coordinates; __hash__ consistent with it; __add__ returning a new vector; and __abs__ returning the length, which math.hypot(x, y) computes.
Test script and expected output:
a = Vector(3, 4)b = Vector(1, 2)print(a + b)print(abs(a))print(a == Vector(3, 4))print(a + b == Vector(4, 6))print({a, Vector(3, 4)})Vector(4, 6)5.0TrueTrue{Vector(3, 4)}Show solution
import math
class Vector: def __init__(self, x, y): self.x = x self.y = y
def __repr__(self): return f"Vector({self.x}, {self.y})"
def __eq__(self, other): if not isinstance(other, Vector): return NotImplemented return self.x == other.x and self.y == other.y
def __hash__(self): return hash((self.x, self.y))
def __add__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x + other.x, self.y + other.y)
def __abs__(self): return math.hypot(self.x, self.y)
a = Vector(3, 4)b = Vector(1, 2)print(a + b)print(abs(a))print(a == Vector(3, 4))print(a + b == Vector(4, 6))print({a, Vector(3, 4)})Vector(4, 6)5.0TrueTrue{Vector(3, 4)}Because __eq__ and __hash__ agree, the set holds one vector, not two. Because __add__ builds a new Vector and leaves its operands alone, a still has its original coordinates for the later lines.
Write a class Team(name, members) that stores the members in a private list. Make len(team) return the number of members, make for member in team loop over them, and give it a __repr__ that shows Team('Docs', ['Asha', 'Ben']). You do not need to write anything for the in operator.
Create Team("Docs", ["Asha", "Ben"]), then print len(team), the two results of "Ben" in team and "Chi" in team on one line, the upper-case form of every member, and the team itself. Expected output:
2True FalseASHABENTeam('Docs', ['Asha', 'Ben'])Show solution
class Team: def __init__(self, name, members): self.name = name self._members = list(members)
def __len__(self): return len(self._members)
def __iter__(self): return iter(self._members)
def __repr__(self): return f"Team({self.name!r}, {self._members!r})"
team = Team("Docs", ["Asha", "Ben"])print(len(team))print("Ben" in team, "Chi" in team)for member in team: print(member.upper())print(team)2True FalseASHABENTeam('Docs', ['Asha', 'Ben'])The !r in the f-string applies repr to the name and the list, which is why the output has quotes. "Ben" in team works through __iter__, as the lesson explained.
Here Tag defines __eq__ and __hash__ from its label. Before running the script, write what each of the three lines prints, and explain how the last one differs from the first two.
class Tag: def __init__(self, label): self.label = label
def __eq__(self, other): return self.label == other.label
def __hash__(self): return hash(self.label)
a = Tag("py")b = Tag("py")print(a is b)print(a == b)print(len({a, b}))Show solution
FalseTrue1a is b asks about identity, and these are two separate objects, so it is False. a == b calls __eq__, which compares labels, so it is True. The set discards the second tag because the two have equal hashes and compare equal, so len is 1.
Summary
- Python implements
len(),print,==,<,+,forand more by calling dunder methods on your object. You write them and let Python call them. __repr__is the developer view and the fallback for__str__.__str__is the friendly text used byprintand f-strings.__eq__compares by value. If you define it, define__hash__as well, over the same fields, and only for objects that do not change.__lt__is enough forsorted,min,maxand for>.__len__and__iter__make a class sized, loop-able and usable within. The iterators lesson explains what__iter__must return.__getitem__givesobj[i]and slices,__contains__customisesin,__bool__sets truthiness, and__call__makes an instance callable.- Return
NotImplemented, not an exception, for operands you do not support. - Going further: the advanced lesson Descriptors shows how properties work underneath.