Magic/Dunder Methods

Difficulty: Intermediate

Question

What are dunder (magic) methods? How do they enable operator overloading?

Answer

Dunder (double underscore) methods are special methods that Python calls implicitly to perform operations on objects. They enable operator overloading and protocol implementation.

Common categories: - Construction: __init__, __new__, __del__ - String representation: __str__ (user-friendly), __repr__ (developer-friendly) - Comparison: __eq__, __lt__, __le__, __gt__, __ge__, __ne__ - Arithmetic: __add__, __sub__, __mul__, __truediv__ - Container: __len__, __getitem__, __setitem__, __contains__ - Context: __enter__, __exit__ - Callable: __call__

Code examples

String Representation and Comparison

from functools import total_ordering

@total_ordering  # Generates __le__, __gt__, __ge__ from __eq__ and __lt__
class Money:
    def __init__(self, amount, currency='USD'):
        self.amount = amount
        self.currency = currency
    
    def __repr__(self):
        return f"Money({self.amount}, '{self.currency}')"
    
    def __str__(self):
        return f"${self.amount:.2f} {self.currency}"
    
    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.amount == other.amount and self.currency == other.currency
    
    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Cannot compare different currencies")
        return self.amount < other.amount
    
    def __hash__(self):
        return hash((self.amount, self.currency))

m1 = Money(10.50)
m2 = Money(20.00)
print(str(m1))     # '$10.50 USD'
print(repr(m2))    # "Money(20.0, 'USD')"
print(m1 < m2)     # True
print(m1 >= m2)    # False (from @total_ordering)

__str__ is for end users (print), __repr__ is for developers (debug). Return NotImplemented (not raise) to let Python try the reflected operation.

Arithmetic Operator Overloading

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented
    
    def __rmul__(self, scalar):
        return self.__mul__(scalar)  # 3 * v == v * 3
    
    def __abs__(self):
        return (self.x  2 + self.y  2)  0.5
    
    def __bool__(self):
        return self.x != 0 or self.y != 0
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)      # Vector(4, 6)
print(v1 * 3)       # Vector(9, 12)
print(3 * v1)       # Vector(9, 12) - uses __rmul__
print(abs(v1))      # 5.0
print(bool(Vector(0, 0)))  # False

__rmul__ handles the case where the left operand doesn't support the operation (3 * v). __abs__ is called by abs(), __bool__ by bool() and in if statements.

Container Protocol (__len__, __getitem__)

class Playlist:
    def __init__(self, name, songs=None):
        self.name = name
        self._songs = songs or []
    
    def __len__(self):
        return len(self._songs)
    
    def __getitem__(self, index):
        return self._songs[index]  # Supports indexing AND slicing
    
    def __setitem__(self, index, value):
        self._songs[index] = value
    
    def __contains__(self, song):
        return song in self._songs
    
    def __iter__(self):
        return iter(self._songs)
    
    def __repr__(self):
        return f"Playlist('{self.name}', {len(self)} songs)"

pl = Playlist("Road Trip", ["Song A", "Song B", "Song C", "Song D"])
print(len(pl))           # 4
print(pl[0])             # 'Song A'
print(pl[-1])            # 'Song D'
print(pl[1:3])           # ['Song B', 'Song C']
print("Song B" in pl)    # True
for song in pl:
    print(song, end=', ')  # Song A, Song B, Song C, Song D,

Implementing __len__ and __getitem__ makes your class work with len(), indexing, slicing, and for loops. __contains__ enables the 'in' operator.

Key points

Concepts covered

__init__, __str__, __repr__, __eq__, __lt__, __add__, __len__, __getitem__