Topic 8.7
Special Methods: The Python Data Model
In one line
Special methods (dunder methods) such as __repr__, __eq__, __len__, __iter__, __add__ and __call__ are hooks that Python calls for you when your object is printed, compared, measured, looped over, added or called. Implementing them makes your own classes behave like built-in types.
Think of it like this
A power socket. Any plug with the right pins fits, and the appliance just works, because the socket and plug follow an agreed shape. Python's built-in operations are sockets: len(x), x + y, for item in x, print(x). A special method is a pin on your class that fits one of those sockets.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Special (dunder) method
- A method with double underscores that Python calls for you in response to syntax or a built-in function.
- Data model
- The full set of special methods that define how objects work with Python's syntax and built-ins.
- `__repr__`
- Returns developer-facing text for an object, ideally valid code that rebuilds it.
- `__str__`
- Returns user-facing text for an object. Used by
printandstr(). - `NotImplemented`
- A special value returned by an operator method meaning "I do not know how to handle this type", so Python tries the other side.
- Reflected method
- A method like
__radd__that Python tries on the right operand when the left one cannot handle the operation. - Callable
- Anything you can call with brackets: functions, classes, and objects with
__call__. - Hashable
- Can be used as a dictionary key or set member. Needs
__hash__and a matching__eq__. - Truthiness
- Whether an object counts as
TrueorFalseinif. Decided by__bool__, then__len__.
Step by step
01Python calls the dunder, not you
When you write len(p), Python looks up __len__ on the class of p and calls it. When you write a + b, it calls type(a).__add__(a, b). You write the method once; every use of the syntax runs it.
The table in the diagram shows the common pairs.
02repr for developers, str for users
In the Vector example, repr(a) gives Vector(3, 4) and str(a) gives <3, 4>. A list shows its items with repr, which is why [a, b] prints with Vector(...). In an f-string, {a!r} forces repr.
Rule of thumb: always write __repr__. Add __str__ only if you want a different, friendlier text.
03Equality and hashing go together
Vector(3, 4) == Vector(3, 4) is True because __eq__ compares fields. The check isinstance(other, Vector) guards against comparing to unrelated types, returning NotImplemented so a == "x" becomes False politely.
Because we also defined __hash__ from the same fields, equal vectors collapse to one set member. Without __hash__, Python makes the class unhashable and reports TypeError: unhashable type.
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))04Operators and their reflected twins
a + b calls a.__add__(b). 2 * a first tries int.__mul__(2, a), which says NotImplemented, and then calls a.__rmul__(2). We set __rmul__ = __mul__ so multiplication works in both orders.
sum([a, b, c]) computes 0 + a first. Our __radd__ accepts 0 and returns the vector, so the sum works. Without it you get a TypeError about unsupported operand types.
05Behave like a collection
The Playlist class defines __len__, __getitem__, __contains__ and __iter__. Now len(p), p[0], p[0:2] (slices are passed to __getitem__), "waves" in p and for song in p all work, as do sorted(p) and reversed(p).
Notice bool(Playlist("empty", [])) is False: with no __bool__, Python uses __len__, and zero means falsy.
06Ordering
Version defines __eq__ and __lt__ and is decorated with @total_ordering, which fills in the rest (>, <=, >=). Comparing the tuple of ints makes 1.10.0 greater than 1.2.10, which text comparison gets wrong.
07Callable objects
Greeter("Hello") returns an object that stores a greeting. hello("Asha") calls __call__. This is handy when you need a function that carries settings, such as an API client with a key, or a retry policy.
Try it yourself
- 1
Print three ways
In the first example, comment out
__str__and run:print(a)now shows thereprtext. Then comment out__repr__too and you get the ugly default with a memory address (do this on your own machine, not for the output check). - 2
Add subtraction
Add
__sub__soa - bworks and prints<2, 2>. Then ask yourself: do you need__rsub__? Only if the left side could be a type that does not know about vectors. - 3
Make the playlist truthy
Add
def __bool__(self): return TruetoPlaylistand printbool(Playlist("empty", [])). An explicit__bool__beats__len__.
Code & diagrams
Remove __radd__ and run again to read the TypeError that sum() raises. Then add __sub__.
Expected output
Vector(3, 4) <3, 4> <3, 4>
[Vector(3, 4), Vector(1, 2)]
<4, 6>
<6, 8> <6, 8>
<14, 16>
True False False
2
5.0
False True
Vector(3, 4) and <3, 4>
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'Try list(p), min(p) and p[::-1]. All work because the class follows the container protocol.
Expected output
3 rain wind ['rain', 'waves']
True False
playing rain
playing waves
playing wind
['rain', 'waves', 'wind'] ['wind', 'waves', 'rain']
False
[Version('1.2.0'), Version('1.2.10'), Version('1.10.0')]
True True
Version('1.10.0')
Hello, Asha! TrueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Define __eq__ and then use the object in a set
Write __eq__ without __hash__ and add the object to a set.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
seen = {Point(1, 2)}Break #2
Add two objects that have no __add__
Write Vector(1, 2) + Vector(3, 4) on a class without __add__.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
print(Vector(1, 2) + Vector(3, 4))Myth vs fact
Myth
You call __len__ and friends yourself.
Fact
You call len(x); Python calls __len__. Calling dunders directly is only for super().__init__() and a few special cases.
Myth
__str__ and __repr__ are the same thing.
Fact
__repr__ is for developers and should be unambiguous; __str__ is for readers. print uses __str__ if it exists and otherwise falls back to __repr__.
Myth
Return False from __eq__ for unknown types.
Fact
Return NotImplemented so Python can try the other operand's method. If both sides return NotImplemented, == falls back to an identity check (a is b), which is False for two different objects.
Myth
Special methods can be added to an instance to change its behaviour.
Fact
Python looks them up on the class, not the instance. Assigning obj.__len__ = ... has no effect on len(obj).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Special methods are looked up on the type (
type(x).__len__(x)), skipping the instance dictionary. This is faster and is what lets built-ins likelenwork on any object consistently. - ▸
The
__eq__/__hash__contract: ifa == bthenhash(a) == hash(b). Mutating a field used in__hash__while the object is in a set or dict key position loses it silently. Use frozen dataclasses (Topic 8.8) for hashable values. - ▸
Operator details interviewers probe:
!=is derived from__eq__automatically (you rarely write__ne__). If the right operand's class is a subclass of the left operand's class and overrides the reflected method, Python tries the right side first. In-place operators (+=) try__iadd__and fall back to__add__plus rebinding, which is why+=mutates a list but rebinds a tuple. - ▸
Hash consistency goes across types:
hash(1) == hash(1.0) == hash(True)because1 == 1.0 == True, so{1: "a", 1.0: "b"}has one key. If your type compares equal to another type, its hash must match theirs too. - ▸
NumPy arrays, pandas Series and PyTorch tensors all use the data model heavily:
__add__,__getitem__with slices and masks,__matmul__(@) for matrix multiplication. Knowing the protocol makes their behaviour predictable. - ▸
__repr__is your first debugging tool. Add it to every class you write; log lines and test failure messages showrepr, so a clear one saves time. Avoid__str__that can raise, because it can fail inside the code that is trying to report an error.
Remember this
- 1
Special methods are also called dunder methods (double underscore, like
__len__). You almost never call them directly. Python does it:len(p)callsp.__len__(), anda + bcallsa.__add__(b). The full set of these hooks is called the Python data model, and it is why user-made classes can feel as natural as lists and numbers. - 2
__repr__returns an unambiguous text for developers, ideally looking like code that rebuilds the object:Vector(3, 4). It is used by the REPL, byrepr(), and when an object is inside a list.__str__returns a readable text for users and is whatprintuses. If only__repr__exists,printuses it too, so write__repr__first. - 3
__eq__defines==. By default two objects are equal only if they are the very same object. ReturnNotImplemented(notFalse) when the other side is a type you do not understand, so Python can try the other side's method. A hash is a whole number that dictionaries and sets use to find an item fast (Topic 4.5). Equal objects must have equal hashes. So when you define__eq__, Python sets__hash__toNone, and the object becomes unhashable. If you want objects as dictionary keys or set members, define__hash__too, from the same fields (hash((self.x, self.y))). Then keep those fields unchanged. - 4
For ordering, define
__lt__and use the decoratorfunctools.total_ordering(decorators: Topic 9.6) to get<=,>, and>=from__lt__and__eq__. Or use dataclasses withorder=True(Topic 8.8). Sorting only needs__lt__. - 5
Make an object behave like a collection with
__len__(thelen()function),__getitem__(indexing and slicing),__contains__(theinoperator) and__iter__(theforloop; Topic 9.1). Python has fallbacks: with only__getitem__,forstill works, because Python triesobj[0],obj[1], ... untilIndexError. Also,bool(obj)uses__bool__and falls back to__len__, so an object with length zero is falsy (Topic 1.4). - 6
Operators map to methods:
+to__add__,*to__mul__,-to__sub__,abs()to__abs__. If the left side cannot handle the right side's type, Python tries the reflected method on the right side:5 + vtriesint.__add__, fails, then callsv.__radd__(5). That is howsum([v1, v2])works:sumstarts from0, so__radd__must accept0. - 7
__call__makes an object callable like a function:hello("Asha")runshello.__call__("Asha"). It is useful for objects that carry configuration or state but are used as functions. A model client or a retry wrapper can be a callable object. Do not overuse special methods: add the ones whose meaning is obvious to every reader.
Explain it without notes
What are special (dunder) methods and who calls them?
What is the difference between __repr__ and __str__?
Why does defining __eq__ make a class unhashable, and how do you fix it?
How does sum([v1, v2]) work with a custom class, and why is __radd__ needed?
Practice
Write a class Money with amount and currency. Add __repr__, __eq__, and __add__ (only for the same currency; otherwise return NotImplemented). Print Money(5, "INR") + Money(7, "INR").
Write a class Team with a list of names, supporting len(team), "Asha" in team and for name in team. Print each.
Write a class Multiplier whose objects are called like functions: triple = Multiplier(3), then triple(5) returns 15. Use it with list(map(triple, [1, 2, 3])).
Trade-offs
- ↔
Special methods make your classes feel built-in, but every one is a promise. If
__eq__or__add__surprises readers, the clever syntax costs more than a named method would. - ↔
Operator overloading is powerful for maths-like types (vectors, money, versions). For anything else a plain method with a clear name (
merge,combine) is easier to read and search for.
Done when you can
Done when you can write
__repr__for any class you create and explain how it differs from__str__.Done when you can explain the
__eq__and__hash__contract.Done when you can make a class work with
len, indexing,inandfor.Done when you can explain reflected methods such as
__radd__andNotImplemented.Done when you can make an object callable with
__call__.