100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Magic (Dunder) Methods in Python

How Python's double-underscore methods let your classes plug into built-in syntax like print(), ==, len(), and hashing.

OOP AdvancedIntermediate12 min readJul 7, 2026
Analogies

1. Introduction

Magic methods (also called dunder methods, short for 'double underscore') are special methods surrounded by double underscores, such as __init__, __str__, and __add__. Python calls them automatically in response to built-in operations and syntax — you rarely call them directly. They are the mechanism that lets a custom class behave like a built-in type: support printing, equality checks, iteration, indexing, arithmetic, and more.

🏏

Cricket analogy: A custom Player class defining __add__ lets you write player1 + player2 to combine their stats automatically, just as Python calls __init__ behind the scenes the moment you draft a new player object.

Because dunder methods are invoked implicitly by the interpreter (e.g. print(obj) triggers __str__, obj1 == obj2 triggers __eq__), they are the primary way Python implements 'protocols' — informal interfaces that objects opt into by defining the right set of methods. This is the foundation of Python's data model.

🏏

Cricket analogy: print(player) silently triggering __str__ is like a scoreboard operator automatically formatting a player's name for display without being explicitly told how -- dunder methods are Python's version of an agreed-upon protocol.

2. Syntax

python
class MyClass:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        # Unambiguous, developer-facing representation
        return f"MyClass({self.value!r})"

    def __str__(self):
        # Readable, user-facing representation
        return f"MyClass with value {self.value}"

    def __eq__(self, other):
        if not isinstance(other, MyClass):
            return NotImplemented
        return self.value == other.value

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

    def __len__(self):
        return len(str(self.value))

3. Explanation

Common dunder methods fall into families: object creation (__init__, __new__), string representation (__str__, __repr__), comparison (__eq__, __lt__, __le__, ...), container behavior (__len__, __getitem__, __setitem__, __contains__), numeric operators (__add__, __sub__, __mul__), and context managers (__enter__, __exit__). Python looks these up on the class, not the instance, so overriding them on the class is what changes built-in behavior.

🏏

Cricket analogy: __init__ drafts a new Player, __str__/__repr__ format how they're announced, __eq__/__lt__ rank them by average, __len__/__getitem__ let you index a Team's squad, and __add__ combines stats -- all defined on the Player class, not on any one instance.

__str__ vs __repr__: __repr__ should return an unambiguous string that (ideally) could recreate the object, and is used by repr(), the interactive shell, and inside containers (e.g. printing a list of objects). __str__ is used by str() and print() for a human-friendly display. If __str__ is not defined, Python falls back to __repr__. Always define __repr__ at minimum; add __str__ only when a different, friendlier display is useful.

Gotcha: overriding __eq__ without also overriding __hash__ makes instances unhashable. Python's default __hash__ is based on identity (id()), which is inconsistent with a custom __eq__ that compares values. To prevent silently-broken hashing, Python sets __hash__ to None automatically when you define __eq__ but not __hash__ — instances then raise TypeError('unhashable type') if you try to put them in a set or use them as dict keys. Define __hash__ explicitly (usually hash of the same fields used in __eq__) whenever you override __eq__ and still need the object to be hashable.

4. Example

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

    def __len__(self):
        return int((self.x ** 2 + self.y ** 2) ** 0.5)


p1 = Point(3, 4)
p2 = Point(3, 4)
p3 = Point(1, 1)

print(p1)
print(repr(p1))
print(p1 == p2)
print(p1 == p3)
print(len(p1))
print(len({p1, p2, p3}))

5. Output

text
(3, 4)
Point(3, 4)
True
False
5
2

6. Key Takeaways

  • Dunder methods let custom objects integrate with built-in syntax like print(), ==, len(), and for loops.
  • __repr__ is for developers/debugging; __str__ is for end-user display; __str__ falls back to __repr__ if undefined.
  • Overriding __eq__ without __hash__ makes an object unhashable — define both together for value-based equality.
  • Comparison and arithmetic dunder methods should return NotImplemented (not raise) when the other operand's type isn't supported.
  • Python calls dunder methods implicitly; you almost never call obj.__str__() directly — use str(obj) instead.
  • set() and dict deduplication/lookup rely on both __eq__ and __hash__ being consistent with each other.

Practice what you learned

Was this page helpful?

Topics covered

#Python#PythonProgrammingStudyNotes#Programming#MagicDunderMethodsInPython#Magic#Dunder#Methods#Syntax#Functions#StudyNotes#SkillVeris