Dunder Methods

Python 3.14 · ✓ verified by execution on 2026-07-26

When you use Python’s built-in syntax—like adding two numbers with +, checking length with len(), or printing an object—Python handles the operation by looking for specific methods behind the scenes. These special methods are often called dunder methods because their names begin and end with “double underscores”, such as __init__ or __str__.

By defining dunder methods in your own classes, you can seamlessly integrate your custom objects with Python’s built-in functions and operators. This is what gives Python its distinctive, readable flair.

String Representations

The most common dunder methods you will use dictate how an object is converted into a string. Python provides two distinct string representations: the official representation (__repr__) and the informal representation (__str__).

The Official Representation: __repr__

The __repr__ method computes the “official” string representation of an object. Its goal is to be unambiguous and helpful for developers who are debugging. If at all possible, the string returned by __repr__ should look like a valid Python expression that could be used to recreate the object.

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f'Point({self.x}, {self.y})'

p = Point(1, 2)
print(repr(p))
Output
Point(1, 2)

The Informal Representation: __str__

While __repr__ is for developers, the __str__ method computes the “informal” or nicely printable string representation of an object. This is what users should see. It is automatically called when you pass your object to print() or str().

python
class User:
    def __init__(self, username):
        self.username = username
    def __str__(self):
        return f'User: @{self.username}'

u = User('alice123')
print(u)
Output
User: @alice123

If you define __repr__ but omit __str__, Python will fall back to using your __repr__ method for printing. However, if you only define __str__, collections like lists will still use the default memory-address representation when printing their elements. It is generally a best practice to always define __repr__.

Customizing Equality

By default, if you use the == operator on two custom objects, Python simply checks if they are the exact same object in memory (their identity). If you want to compare their actual values, you must override the __eq__ method.

The __eq__ method is part of Python’s “rich comparison” toolkit. When you evaluate x == y, Python secretly calls x.__eq__(y).

python
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def __eq__(self, other):
        if not isinstance(other, Item):
            return False
        return self.name == other.name and self.price == other.price

i1 = Item('Apple', 1.0)
i2 = Item('Apple', 1.0)
print(i1 == i2)
Output
True

Notice that we first check if the other object is actually an instance of Item. Failing to check isinstance() can lead to an AttributeError if your object is accidentally compared to a totally different type, like an integer or a string.

Emulating Containers

If you want your object to behave like a collection (such as a list or a dictionary), you can define methods that Python expects containers to have. For example, the __len__ method allows custom objects to be passed to the built-in len() function.

python
class Team:
    def __init__(self, members):
        self.members = members
    def __len__(self):
        return len(self.members)

t = Team(['Alice', 'Bob', 'Charlie'])
print(len(t))
Output
3

If __len__ returns a negative number or a non-integer, Python will raise an error.

Operator Overloading

Operator overloading allows your custom objects to use standard mathematical symbols like +, -, or *. You implement this by defining arithmetic dunder methods.

For instance, to evaluate the expression x + y, Python invokes the __add__ method. This allows you to construct intuitive interfaces for mathematical types like Vectors.

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    def __str__(self):
        return f'<{self.x}, {self.y}>'

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)
Output
<4, 6>

You can also combine your object with standard Python data types. If we want to multiply a vector by an integer scalar, we can implement the __mul__ method (*).

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    def __str__(self):
        return f'<{self.x}, {self.y}>'

v = Vector(2, 3)
print(v * 4)
Output
<8, 12>

Dunder methods are the secret sauce that makes Python feel cohesive. By hooking into the data model, your custom classes can participate in Python’s ecosystem exactly like the built-in types do.

Check yourself

Why should you define `__repr__` even if you already have `__str__` defined?

Reveal answer

Collections like lists will always use `__repr__` when displaying their elements, ignoring `__str__`. — While `print()` and `str()` fall back to `__repr__` if `__str__` is missing, the reverse is not true. Collections like lists always call `__repr__` on their elements, making `__repr__` essential for debugging and proper display.

What is the default behavior when comparing two custom objects with `==` if `__eq__` is not defined?

Reveal answer

It compares the objects' memory identities, exactly like the `is` operator. — By default, custom objects inherit `__eq__` from the base `object` class, which simply compares memory addresses (identity). You must explicitly override `__eq__` to compare them by value.

How are dunder methods meant to be executed?

Reveal answer

Indirectly, by using built-in operators (like `+`) and functions (like `len()`). — While you can call them directly, dunder methods are intended to hook into Python's built-in syntax. For instance, `len(obj)` automatically calls `obj.__len__()`, and `a + b` automatically calls `a.__add__(b)`.

Does defining `__eq__` automatically create a working `__ne__` (not equal) method?

Reveal answer

Yes, in Python 3, `__ne__` automatically falls back to the inverse of `__eq__`. — In Python 3, `__ne__` does default to the inverse of `__eq__`. However, for other comparison operators like `<` or `>`, you must define them manually or use the `functools.total_ordering` decorator.

Challenges

Challenge 1 +50 XP

Fix the bug! The `Product` class overrides `__eq__` so that two products with the same name are considered equal. However, checking `product == 'Apple'` crashes because a string doesn't have a `.name` attribute. Use `isinstance()` to fix `__eq__` so it returns `False` if `other` is not a `Product`.

python
  • Test 1 — expects "False\n"
Need a hint? (−25% XP)

Add `if not isinstance(other, Product): return False` at the beginning of the `__eq__` method.

Show solution (0 XP)
class Product:
    def __init__(self, name):
        self.name = name
        
    def __eq__(self, other):
        if not isinstance(other, Product):
            return False
        return self.name == other.name

p = Product('Apple')
print(p == 'Apple')

Challenge 2 +100 XP

Create a `Wallet` class that holds a number of `coins`. Implement the `__add__` dunder method so that adding two wallets returns a new `Wallet` with the combined total of coins. Implement `__str__` to print `Wallet with X coins`.

python
  • Test 1 — expects "Wallet with 25 coins\n"
Need a hint? (−25% XP)

Define `def __add__(self, other): return Wallet(self.coins + other.coins)` and `def __str__(self): return f'Wallet with {self.coins} coins'`.

Show solution (0 XP)
class Wallet:
    def __init__(self, coins):
        self.coins = coins
        
    def __add__(self, other):
        return Wallet(self.coins + other.coins)
        
    def __str__(self):
        return f'Wallet with {self.coins} coins'

w1 = Wallet(10)
w2 = Wallet(15)
print(w1 + w2)