Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~28 min· topic 7 of 12

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 print and str().
`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 True or False in if. 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.

Python calls the dunder, not youdiagram
Rendering diagram…

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.

main.pywhole filepython
    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. 1

    Print three ways

    In the first example, comment out __str__ and run: print(a) now shows the repr text. 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. 2

    Add subtraction

    Add __sub__ so a - b works 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. 3

    Make the playlist truthy

    Add def __bool__(self): return True to Playlist and print bool(Playlist("empty", [])). An explicit __bool__ beats __len__.

Code & diagrams

A Vector that works with print, ==, +, * and sum New tab

Remove __radd__ and run again to read the TypeError that sum() raises. Then add __sub__.

Sign in to run this example in your browser.

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'
A container, ordering and a callable New tab

Try list(p), min(p) and p[::-1]. All work because the class follows the container protocol.

Sign in to run this example in your browser.

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! True
Syntax to special methoddiagram
Rendering diagram…

Break 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.

main.pywhole filepython
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)}
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 10, in <module>
seen = {Point(1, 2)}
^^^^^^^^^^^^^
TypeError: unhashable type: 'Point'

Break #2

Add two objects that have no __add__

Write Vector(1, 2) + Vector(3, 4) on a class without __add__.

main.pywhole filepython
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y


print(Vector(1, 2) + Vector(3, 4))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 7, in <module>
print(Vector(1, 2) + Vector(3, 4))
~~~~~~~~~~~~~^~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'

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 like len work on any object consistently.

  • ▸

    The __eq__ / __hash__ contract: if a == b then hash(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) because 1 == 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 show repr, 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. 1

    Special methods are also called dunder methods (double underscore, like __len__). You almost never call them directly. Python does it: len(p) calls p.__len__(), and a + b calls a.__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. 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, by repr(), and when an object is inside a list. __str__ returns a readable text for users and is what print uses. If only __repr__ exists, print uses it too, so write __repr__ first.

  3. 3

    __eq__ defines ==. By default two objects are equal only if they are the very same object. Return NotImplemented (not False) 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__ to None, 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. 4

    For ordering, define __lt__ and use the decorator functools.total_ordering (decorators: Topic 9.6) to get <=, >, and >= from __lt__ and __eq__. Or use dataclasses with order=True (Topic 8.8). Sorting only needs __lt__.

  5. 5

    Make an object behave like a collection with __len__ (the len() function), __getitem__ (indexing and slicing), __contains__ (the in operator) and __iter__ (the for loop; Topic 9.1). Python has fallbacks: with only __getitem__, for still works, because Python tries obj[0], obj[1], ... until IndexError. Also, bool(obj) uses __bool__ and falls back to __len__, so an object with length zero is falsy (Topic 1.4).

  6. 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 + v tries int.__add__, fails, then calls v.__radd__(5). That is how sum([v1, v2]) works: sum starts from 0, so __radd__ must accept 0.

  7. 7

    __call__ makes an object callable like a function: hello("Asha") runs hello.__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

01

What are special (dunder) methods and who calls them?

02

What is the difference between __repr__ and __str__?

03

Why does defining __eq__ make a class unhashable, and how do you fix it?

04

How does sum([v1, v2]) work with a custom class, and why is __radd__ needed?

Practice

01

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").

02

Write a class Team with a list of names, supporting len(team), "Asha" in team and for name in team. Print each.

03

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, in and for.

  • Done when you can explain reflected methods such as __radd__ and NotImplemented.

  • Done when you can make an object callable with __call__.