Magic (Dunder) Methods
Learn how double-underscore methods like __init__, __str__, __len__, __eq__, and __add__ let custom objects work with built-in Python syntax.
Introduction
Have you ever wondered how print(my_list) shows readable content, how len("hello") works, or how 3 + 4 and "a" + "b" both work with the same + symbol? Behind the scenes, Python objects implement special methods with double underscores before and after their names — commonly called "dunder" methods (short for "double underscore"), or sometimes "magic methods."
In this lesson, you will learn what dunder methods are and why they matter, and implement several of the most important ones — __init__, __str__, __repr__, __len__, __eq__, __add__, and a preview of __getitem__ — on your own custom class.
- What dunder methods are and why they let objects work with built-in syntax.
- The difference between __str__ and __repr__.
- How __len__ lets len() work on your own objects.
- How __eq__ lets == compare your objects meaningfully.
- How __add__ lets + work with your custom objects.
- A preview of __getitem__ as groundwork for the next lesson on Iterators.
What Are Dunder Methods?
Dunder methods are special methods whose names begin and end with two underscores, such as __init__ or __str__. Python calls these methods automatically in response to specific built-in operations — creating an object, printing it, checking its length, comparing it with ==, or adding it with +.
You never call a dunder method directly by typing obj.__str__() in normal code (though you technically could) — instead, you write ordinary Python syntax like print(obj), and Python looks up and calls the matching dunder method behind the scenes.
__init__
Called automatically when a new object is created.
__str__ / __repr__
Control how an object is displayed as text.
__len__
Lets len(obj) work on your custom objects.
__eq__
Lets == compare two objects using your own logic.
__add__
Lets + combine two objects using your own logic.
__getitem__
Lets obj[index] retrieve items from your custom objects.
__init__: The Constructor (Recap)
You already know __init__ well from earlier lessons — it is itself a dunder method, called automatically whenever a new object is created, used to set up the object's initial attributes.
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
b = Book("Python Basics", 320)
print(b.title, b.pages)Python Basics 320__init__ is the first dunder method most Python learners meet, and it sets the pattern for all the others: a special name, surrounded by double underscores, triggered automatically by ordinary syntax (here, calling Book(...)).
__str__ vs __repr__
Without any dunder methods for display, printing an object shows an unhelpful default like <__main__.Book object at 0x000001>. Defining __str__ and/or __repr__ lets you control what that text looks like.
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __str__(self):
return f"'{self.title}' ({self.pages} pages)"
def __repr__(self):
return f"Book(title={self.title!r}, pages={self.pages})"
b = Book("Python Basics", 320)
print(b) # uses __str__
print(str(b)) # uses __str__ explicitly
print(repr(b)) # uses __repr__ explicitly
b # in an interactive shell, this would show __repr__'Python Basics' (320 pages)
'Python Basics' (320 pages)
Book(title='Python Basics', pages=320)__str__ is meant to be readable and user-friendly (used by print() and str()). __repr__ is meant to be unambiguous and developer-focused, ideally readable enough that eval(repr(obj)) could recreate the object. If only __repr__ is defined, Python falls back to it for print() too.
__len__: Custom Length
Defining __len__ lets the built-in len() function work on instances of your own class, just like it works on strings, lists, and dictionaries.
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
p = Playlist(["Song A", "Song B", "Song C"])
print(len(p)) # calls p.__len__() behind the scenes3__eq__: Custom Equality
By default, == on two custom objects checks whether they are literally the same object in memory — two separately-created objects with identical data would compare as unequal. Defining __eq__ lets you specify what "equal" should mean for your class instead.
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
p1 = Point(2, 3)
p2 = Point(2, 3)
p3 = Point(5, 9)
print(p1 == p2) # True: same x and y, thanks to our __eq__
print(p1 == p3) # False: different coordinatesTrue
False__add__: Operator Overloading
You previewed this in the Polymorphism lesson: defining __add__ lets the + operator work sensibly with your own custom objects, following whatever "addition" means for that class.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2) # calls p1.__add__(p2) behind the scenes(4, 6)__getitem__: A Preview of Indexing
Defining __getitem__ lets your objects support square-bracket indexing, obj[index], just like lists and strings do. This is a small preview of a bigger idea explored fully in the next lesson: implementing __getitem__ (or the related iterator protocol) is part of what makes an object indexable and iterable.
class Playlist:
def __init__(self, songs):
self.songs = songs
def __getitem__(self, index):
return self.songs[index]
p = Playlist(["Song A", "Song B", "Song C"])
print(p[0])
print(p[2])Song A
Song CA class implementing __getitem__ can even be looped over with a for statement, since Python will keep requesting increasing indices until it hits an IndexError. The next lesson explores this idea properly, along with the dedicated __iter__ and __next__ methods that make an object a true iterator.
Why Dunder Methods Matter
Integration
Custom objects work naturally with print(), len(), ==, +, and [] instead of needing special functions.
Readability
Code that uses your objects reads just like code that uses built-in types.
Consistency
Following Python's expected conventions makes your classes predictable to other developers.
Common Mistakes
- Defining only __str__ and expecting repr(obj) in a shell to use it too — repr() specifically needs __repr__.
- Implementing __eq__ but forgetting that it only works if the other object has compatible attributes — comparing to an unrelated type can raise an AttributeError.
- Calling dunder methods directly (obj.__len__()) instead of using the built-in syntax (len(obj)) they were designed to support.
- Assuming every dunder method must be implemented — only add the ones that make sense for your class.
Best Practices
- Always define __repr__ for classes you plan to debug or log — it should be unambiguous and developer-friendly.
- Define __str__ separately only when you want a different, more user-friendly display than __repr__.
- Implement __eq__ whenever "equal data" should count as "equal objects" for your class.
- Only overload operators like __add__ when the operation has an intuitive, expected meaning for your class.
- Keep dunder method implementations simple and side-effect free — they should behave predictably since Python calls them implicitly.
Frequently Asked Questions
Why are they called "dunder" methods?
"Dunder" is short for "double underscore," referring to the two underscores before and after the method name, like __init__ or __str__.
What is the difference between __str__ and __repr__?
__str__ provides a readable, user-friendly string (used by print() and str()). __repr__ provides an unambiguous, developer-focused string, ideally one that could recreate the object. If __str__ is missing, Python falls back to __repr__.
Do I have to implement every dunder method for my class?
No. Implement only the ones relevant to how your objects will be used — for example, only add __add__ if addition genuinely makes sense for that class.
What happens if I do not define __eq__?
Python uses the default identity-based comparison inherited from object, meaning two separately created objects with identical data will still compare as not equal with ==.
How does __getitem__ relate to iteration?
If a class defines __getitem__, a for loop can use it to fetch items at increasing indices (0, 1, 2, ...) until an IndexError is raised. The next lesson covers this, plus the more standard __iter__/__next__ approach, in full detail.
Key Takeaways
- Dunder methods are special double-underscore methods that Python calls automatically for built-in syntax and functions.
- __init__ runs automatically when an object is created; you have used it since the Constructors lesson.
- __str__ controls user-friendly display (print(), str()); __repr__ controls unambiguous, developer-focused display.
- __len__ enables len(obj); __eq__ enables meaningful == comparisons; __add__ enables the + operator on custom objects.
- __getitem__ enables obj[index] syntax and previews what makes objects iterable, which the next lesson covers in depth.
Summary
Dunder methods are the bridge between your custom classes and Python's built-in syntax. By implementing methods like __str__, __repr__, __len__, __eq__, and __add__, your objects can be printed, measured, compared, and combined just as naturally as strings, lists, and numbers.
Next, you will build on __getitem__ and explore iterators in depth — the mechanism that powers for loops and lets you create your own custom iterable objects.
- You understand what dunder methods are and why Python calls them automatically.
- You can implement __str__ and __repr__ and know the difference between them.
- You can implement __len__ and __eq__ for custom objects.
- You can implement __add__ for operator overloading.
- You have previewed __getitem__ and are ready to learn about iterators.