Web Analytics

Special Methods (Dunder)

Intermediate ~30 min read

Special methods (also called "dunder" methods for "double underscore") let you define how your objects behave with Python's built-in operations. Implement __str__ for print(), __eq__ for ==, __add__ for +, and make your classes feel like native Python types.

What Are Dunder Methods?

Dunder methods have double underscores on both sides: __init__, __str__, __add__. Python calls these automatically when you use operators, built-in functions, or certain syntax on your objects. They're the hook into Python's data model.

String Representation: __str__ and __repr__

Control how your objects appear when printed or displayed. __str__ is for end users, __repr__ is for developers and debugging.

Output
Click Run to execute your code

The Golden Rule for __repr__

Make __repr__ return valid Python code that could recreate the object: eval(repr(obj)) should give you an equivalent object. If you only implement one, implement __repr__ - Python uses it as a fallback for __str__.

Comparison Methods

Make your objects comparable with ==, <, >, and enable sorting. Use @total_ordering to only define __eq__ and __lt__.

Output
Click Run to execute your code

__eq__ and __hash__ Are Linked

If you define __eq__, you should also define __hash__ (or set __hash__ = None to make objects unhashable). Objects that compare equal must have the same hash value - this is required for sets and dict keys.

Arithmetic Operators

Implement +, -, *, and other operators for your objects. This is called operator overloading.

Output
Click Run to execute your code

Regular vs Reverse vs In-place

  • __add__: a + b - called on the left operand
  • __radd__: b + a - called if left doesn't support the operation
  • __iadd__: a += b - in-place, should return self

Container Methods

Make your objects behave like lists, dicts, or other containers. Implement __len__, __getitem__, __iter__, and more.

Output
Click Run to execute your code

Iterator vs Iterable

An iterable has __iter__ and returns an iterator. An iterator has both __iter__ (returns self) and __next__. For simple cases, __iter__ can return iter(self.data) and skip __next__.

Common Mistakes

1. Forgetting to return in __str__ / __repr__

# Wrong - prints instead of returning
class Person:
    def __str__(self):
        print(f"Person: {self.name}")  # Returns None!

# Correct - return a string
class Person:
    def __str__(self):
        return f"Person: {self.name}"

2. Forgetting __hash__ when defining __eq__

# Problem - objects become unhashable
class Point:
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    # No __hash__!

p = Point(1, 2)
{p}  # TypeError: unhashable type: 'Point'

# Solution 1 - implement __hash__
def __hash__(self):
    return hash((self.x, self.y))

# Solution 2 - explicitly unhashable
__hash__ = None  # Can't be in sets/dict keys

3. Not returning self from __iadd__

# Wrong - doesn't return self
class Counter:
    def __iadd__(self, other):
        self.value += other
        # Missing return self!

c = Counter(5)
c += 3  # c becomes None!

# Correct - always return self
def __iadd__(self, other):
    self.value += other
    return self  # Required!

4. Not handling different types in __eq__

# Wrong - crashes on type mismatch
class Point:
    def __eq__(self, other):
        return self.x == other.x  # AttributeError if other isn't Point!

# Correct - check type or return NotImplemented
class Point:
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented  # Let Python try other.__eq__
        return self.x == other.x and self.y == other.y

5. Inconsistent __lt__ and __eq__

# Problem - sorting may give unexpected results
class Score:
    def __lt__(self, other):
        return self.value < other.value

    def __eq__(self, other):
        return self.name == other.name  # Different criterion!

# Both should use the same comparison logic
# Or use @total_ordering and be explicit

Exercise: Fraction Class

Task: Create a Fraction class with dunder methods for arithmetic and comparison.

Requirements:

  • __str__: Return "numerator/denominator" (e.g., "3/4")
  • __repr__: Return "Fraction(num, denom)"
  • __eq__: Check equality (1/2 should equal 2/4)
  • __lt__: Enable sorting
  • __add__: Add fractions (a/b + c/d = (ad + cb) / bd)
  • __mul__: Multiply fractions (a/b * c/d = ac / bd)
Output
Click Run to execute your code
Show Solution
class Fraction:
    def __init__(self, numerator, denominator):
        # Simplify fraction
        gcd = self._gcd(abs(numerator), abs(denominator))
        self.num = numerator // gcd
        self.denom = denominator // gcd

    @staticmethod
    def _gcd(a, b):
        while b:
            a, b = b, a % b
        return a

    def __str__(self):
        return f"{self.num}/{self.denom}"

    def __repr__(self):
        return f"Fraction({self.num}, {self.denom})"

    def __eq__(self, other):
        # Works because fractions are simplified
        return self.num == other.num and self.denom == other.denom

    def __lt__(self, other):
        # Compare by cross multiplication
        return self.num * other.denom < other.num * self.denom

    def __add__(self, other):
        new_num = self.num * other.denom + other.num * self.denom
        new_denom = self.denom * other.denom
        return Fraction(new_num, new_denom)

    def __mul__(self, other):
        return Fraction(self.num * other.num, self.denom * other.denom)


# Test
f1 = Fraction(1, 2)
f2 = Fraction(1, 4)
f3 = Fraction(2, 4)

print(f"f1: {f1}, repr: {repr(f1)}")
print(f"f1 == f3: {f1 == f3}")  # True
print(f"f1 + f2: {f1 + f2}")    # 3/4
print(f"f1 * f2: {f1 * f2}")    # 1/8
print(f"Sorted: {sorted([Fraction(3,4), f1, f2])}")

Summary

  • __str__ for users, __repr__ for developers (implement __repr__ first)
  • __eq__ + __hash__ for equality and set/dict usage
  • @total_ordering: Define __eq__ + __lt__, get all comparisons
  • __add__, __radd__, __iadd__ for +, reverse, and in-place
  • __len__, __getitem__, __iter__ for container behavior
  • Return NotImplemented (not raise NotImplementedError) from operators on type mismatch

What's Next?

Now that you know about dunder methods, learn about Properties - using @property decorator to create getters and setters that look like attribute access, providing cleaner syntax than explicit getter/setter methods.