Skip to content
intermediate Phase 2 · Python OOP

Magic Methods & Dunder

Implement __str__, __repr__, __len__, __eq__, and operator overloading.

1h
0 problems
Topic Progress 0%

String Representation

repr vs str

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        """Unambiguous representation for developers."""
        return f'Point({self.x}, {self.y})'
    
    def __str__(self):
        """Readable representation for users."""
        return f'({self.x}, {self.y})'

p = Point(1, 2)

print(repr(p))  # Point(1, 2) - used in REPL, debuggers
print(str(p))   # (1, 2) - used by print()
print(p)        # (1, 2) - uses __str__

# In collections, __repr__ is used
print([p, Point(3, 4)])
# [Point(1, 2), Point(3, 4)]

Best Practice

# ✅ Always implement __repr__
# __str__ falls back to __repr__ if not defined

class GoodClass:
    def __repr__(self):
        return f'{self.__class__.__name__}({self.__dict__})'

# This gives useful debugging output
gc = GoodClass()
print(repr(gc))  # GoodClass({})

Format Specification

class Color:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b
    
    def __format__(self, format_spec):
        if format_spec == 'hex':
            return f'#{self.r:02x}{self.g:02x}{self.b:02x}'
        elif format_spec == 'rgb':
            return f'rgb({self.r}, {self.g}, {self.b})'
        return f'Color({self.r}, {self.g}, {self.b})'

c = Color(255, 128, 0)
print(f'{c}')       # Color(255, 128, 0)
print(f'{c:hex}')   # #ff8000
print(f'{c:rgb}')   # rgb(255, 128, 0)

Operator Overloading

Arithmetic Operators

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):     # self + other
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):     # self - other
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):    # self * other
        return Vector(self.x * scalar, self.y * scalar)
    
    def __rmul__(self, scalar):   # other * self (reflected)
        return self.__mul__(scalar)
    
    def __truediv__(self, scalar): # self / other
        return Vector(self.x / scalar, self.y / scalar)
    
    def __neg__(self):            # -self
        return Vector(-self.x, -self.y)
    
    def __abs__(self):            # abs(self)
        return (self.x**2 + self.y**2)**0.5
    
    def __repr__(self):
        return f'Vector({self.x}, {self.y})'

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)   # Vector(4, 6)
print(v1 - v2)   # Vector(-2, -2)
print(v1 * 3)    # Vector(3, 6)
print(3 * v1)    # Vector(3, 6) - uses __rmul__
print(v1 / 2)    # Vector(0.5, 1.0)
print(-v1)       # Vector(-1, -2)
print(abs(v1))   # 2.236...

Comparison Operators

class Money:
    def __init__(self, amount, currency='USD'):
        self.amount = amount
        self.currency = currency
    
    def __eq__(self, other):  # self == other
        return self.amount == other.amount and self.currency == other.currency
    
    def __lt__(self, other):  # self < other
        if self.currency != other.currency:
            raise ValueError('Cannot compare different currencies')
        return self.amount < other.amount
    
    def __le__(self, other):  # self <= other
        return self == other or self < other
    
    def __gt__(self, other):  # self > other
        return not self <= other
    
    def __ge__(self, other):  # self >= other
        return not self < other
    
    def __hash__(self):
        return hash((self.amount, self.currency))
    
    def __repr__(self):
        return f'Money({self.amount}, {self.currency}')'

m1 = Money(100, 'USD')
m2 = Money(200, 'USD')

print(m1 < m2)   # True
print(m1 == m2)   # False
print(m1 >= m1)   # True

Container Methods

class Playlist:
    def __init__(self, name):
        self.name = name
        self._songs = []
    
    def __len__(self):           # len(self)
        return len(self._songs)
    
    def __getitem__(self, index): # self[index]
        return self._songs[index]
    
    def __setitem__(self, index, value):  # self[index] = value
        self._songs[index] = value
    
    def __delitem__(self, index): # del self[index]
        del self._songs[index]
    
    def __contains__(self, item): # item in self
        return item in self._songs
    
    def __iter__(self):           # for item in self
        return iter(self._songs)
    
    def add(self, song):
        self._songs.append(song)

playlist = Playlist('My Songs')
playlist.add('Song 1')
playlist.add('Song 2')

print(len(playlist))      # 2
print(playlist[0])        # Song 1
print('Song 1' in playlist)  # True

for song in playlist:
    print(song)

Context Managers & Callables

Context Manager Protocol

class Timer:
    def __init__(self, label):
        self.label = label
    
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.time() - self.start
        print(f'{self.label}: {self.elapsed:.4f} seconds')
        return False  # Don't suppress exceptions

with Timer('Loop'):
    total = sum(range(1000000))
# Loop: 0.0312 seconds

Custom Exception Handling

class ManagedResource:
    def __enter__(self):
        print('Acquiring resource')
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f'Error occurred: {exc_val}')
            # Return True to suppress exception
            # Return False (or None) to re-raise
        print('Releasing resource')
        return False

Callable Objects

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, x):
        return x * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

# Can be used anywhere a function is expected
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers))
# [2, 4, 6, 8, 10]

# Check if object is callable
print(callable(double))   # True
print(callable(42))       # False

Other Useful Magic Methods

class MyClass:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):        # hash(self)
        return hash(self.value)
    
    def __bool__(self):        # bool(self)
        return bool(self.value)
    
    def __sizeof__(self):      # sys.getsizeof(self)
        return object.__sizeof__(self) + 8
    
    def __copy__(self):        # copy.copy(self)
        return self.__class__(self.value)
    
    def __deepcopy__(self, memo):  # copy.deepcopy(self)
        return self.__class__(self.value)

# Usage
obj = MyClass(42)
print(hash(obj))      # 42
print(bool(obj))      # True
print(bool(MyClass(0)))  # False