Special Methods: Making Objects Behave Like Built-ins

Dunder methods let your class work with print, len, +, ==, in, iteration and with. They are how Python asks an object to participate in ordinary syntax.

What a dunder method is

Names surrounded by double underscores are hooks Python calls for you. You almost never call them directly; you use the syntax and Python calls the method.

You writePython calls
len(obj)obj.__len__()
obj[key]obj.__getitem__(key)
a + ba.__add__(b)
a == ba.__eq__(b)
str(obj), print(obj)obj.__str__()
x in objobj.__contains__(x)
for x in objobj.__iter__()
obj(...)obj.__call__(...)
with obj:obj.__enter__() / __exit__()

__str__ and __repr__

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author


b = Book("Dune", "Frank Herbert")
print(b)          # <__main__.Book object at 0x7f...>  - useless
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __repr__(self):
        """Unambiguous, aimed at a developer."""
        return f"Book(title={self.title!r}, author={self.author!r})"

    def __str__(self):
        """Readable, aimed at a user."""
        return f"{self.title} by {self.author}"


b = Book("Dune", "Frank Herbert")

print(b)                 # Dune by Frank Herbert       - uses __str__
print(str(b))            # the same
print(repr(b))           # Book(title='Dune', ...)     - uses __repr__
print([b])               # inside a container, __repr__ is used
print(f"{b}")            # __str__
print(f"{b!r}")          # __repr__
If you write only one, write __repr__. Python falls back to it when __str__ is missing, and it is what appears in lists, dictionaries, the REPL and debugger output - which is exactly where you need it most.

Length, indexing and membership

class Playlist:
    def __init__(self, name):
        self.name = name
        self.tracks = []

    def add(self, track):
        self.tracks.append(track)
        return self

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

    def __getitem__(self, index):
        return self.tracks[index]            # slices work too, for free

    def __setitem__(self, index, value):
        self.tracks[index] = value

    def __delitem__(self, index):
        del self.tracks[index]

    def __contains__(self, track):
        return track in self.tracks

    def __iter__(self):
        return iter(self.tracks)

    def __repr__(self):
        return f"Playlist({self.name!r}, {len(self)} tracks)"


p = Playlist("Focus")
p.add("Track A").add("Track B").add("Track C")

print(len(p))                # 3
print(p[0])                  # Track A
print(p[-1])                 # Track C
print(p[0:2])                # ['Track A', 'Track B']
print("Track B" in p)        # True
for track in p:
    print(" -", track)
print(bool(p))               # True - falls back to __len__
print(p)                     # Playlist('Focus', 3 tracks)

Six small methods made the class work with len, indexing, slicing, in, iteration and truthiness. None of that syntax knows anything about Playlist.

Comparison

from functools import total_ordering


@total_ordering
class Version:
    def __init__(self, major, minor, patch=0):
        self.parts = (major, minor, patch)

    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self.parts == other.parts

    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self.parts < other.parts

    def __hash__(self):
        return hash(self.parts)

    def __repr__(self):
        return "v" + ".".join(str(p) for p in self.parts)


versions = [Version(1, 2), Version(1, 10), Version(0, 9, 3)]

print(sorted(versions))                     # [v0.9.3, v1.2.0, v1.10.0]
print(max(versions))
print(Version(1, 2) == Version(1, 2, 0))    # True
print(Version(2, 0) > Version(1, 99))       # True
print(len({Version(1, 0), Version(1, 0)}))  # 1

Returning NotImplemented for an unknown type is the correct habit: Python then tries the reflected operation on the other object before giving up, and comparisons with unrelated types fall back to sensible defaults.

Arithmetic

class Money:
    def __init__(self, amount, currency="INR"):
        self.amount = amount
        self.currency = currency

    def _check(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if other.currency != self.currency:
            raise ValueError("currency mismatch")
        return other

    def __add__(self, other):
        other = self._check(other)
        if other is NotImplemented:
            return NotImplemented
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        other = self._check(other)
        if other is NotImplemented:
            return NotImplemented
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, factor):
        if not isinstance(factor, (int, float)):
            return NotImplemented
        return Money(self.amount * factor, self.currency)

    __rmul__ = __mul__                      # so 3 * money also works

    def __neg__(self):
        return Money(-self.amount, self.currency)

    def __eq__(self, other):
        return (isinstance(other, Money)
                and (self.amount, self.currency) == (other.amount, other.currency))

    def __hash__(self):
        return hash((self.amount, self.currency))

    def __str__(self):
        return f"{self.currency} {self.amount:,.2f}"

    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"


a = Money(1500)
b = Money(250.50)

print(a + b)               # INR 1,750.50
print(a - b)
print(a * 3)
print(3 * a)               # works because of __rmul__
print(-a)
print(sum([a, b], Money(0)))
OperatorMethodReflected
+__add____radd__
-__sub____rsub__
*__mul____rmul__
/__truediv____rtruediv__
//__floordiv____rfloordiv__
%__mod____rmod__
**__pow____rpow__
+=__iadd__-

Truthiness

class Basket:
    def __init__(self, items=None):
        self.items = items or []

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


print(bool(Basket()))              # False - via __len__
print(bool(Basket(["pen"])))       # True


class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __bool__(self):
        return self.celsius > 0    # explicit rule


print(bool(Temperature(-5)))       # False
print(bool(Temperature(20)))       # True

Python tries __bool__ first, then __len__, and treats the object as true if neither exists.

Callable objects and attribute hooks

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, n):
        return n * self.factor


triple = Multiplier(3)
print(triple(7))                        # 21
print(list(map(triple, [1, 2, 3])))     # [3, 6, 9]
class Config:
    def __init__(self, data):
        self._data = data

    def __getattr__(self, name):
        """Called ONLY when normal lookup fails."""
        try:
            return self._data[name]
        except KeyError:
            raise AttributeError(f"no setting named {name!r}") from None


config = Config({"theme": "dark", "size": 14})
print(config.theme)          # dark
# print(config.missing)      # AttributeError: no setting named 'missing'

A complete small class

class Vector:
    def __init__(self, *components):
        self.components = tuple(components)

    def __repr__(self):
        return f"Vector{self.components}"

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

    def __getitem__(self, index):
        return self.components[index]

    def __iter__(self):
        return iter(self.components)

    def __eq__(self, other):
        return isinstance(other, Vector) and self.components == other.components

    def __hash__(self):
        return hash(self.components)

    def __add__(self, other):
        if len(self) != len(other):
            raise ValueError("vectors must be the same length")
        return Vector(*(a + b for a, b in zip(self, other)))

    def __mul__(self, scalar):
        return Vector(*(c * scalar for c in self.components))

    __rmul__ = __mul__

    def __abs__(self):
        return sum(c * c for c in self.components) ** 0.5


v = Vector(3, 4)
w = Vector(1, 2)

print(v + w)             # Vector(4, 6)
print(v * 2)             # Vector(6, 8)
print(abs(v))            # 5.0
print(len(v), v[0])      # 2 3
print(list(v))           # [3, 4]
print(v == Vector(3, 4)) # True

Common mistakes

  • Writing only __str__, so lists and debugger output stay unreadable.
  • Defining __eq__ without __hash__, making the object unusable in sets.
  • Raising TypeError from an arithmetic dunder instead of returning NotImplemented.
  • Making __eq__ or __hash__ depend on attributes that change.
  • Calling obj.__len__() directly instead of len(obj).
  • Implementing __getattr__ in a way that recurses infinitely by touching a missing attribute.

Best practices

  • Give every class a __repr__ that shows how to rebuild it.
  • Define __eq__ and __hash__ together, over immutable attributes.
  • Return NotImplemented for types you do not handle.
  • Implement only the dunders your class genuinely needs.
  • Keep dunder methods fast and free of side effects; they are called implicitly.

Practice

  1. Add __repr__ and __str__ to a class and show where each one appears.
  2. Write a Matrix class supporting +, * by a scalar and indexing by row and column.
  3. Make a class usable in a set by implementing __eq__ and __hash__.
  4. Write a class that supports in, len and iteration over its internal data.
  5. Implement __call__ so instances can be passed to sorted(key=...).

Conclusion

Dunder methods are how a class joins in with Python's own syntax. Start with __repr__, add __eq__ and __hash__ when identity by value matters, and add the rest only when the syntax genuinely belongs to your object.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Classes and Objects

A class describes a kind of thing; an object is one of them. Python builds every object the same way, and understanding that removes most of the myste...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.