Skip to main content

Module 12 - Special Methods (Dunder Methods & Operator Overloading)

This module covers special methods (dunder methods) in Python. You'll learn how to customize object behavior, overload operators, and implement magic methods for rich functionality.


1. Introduction to Special Methods

1.1 What are Special Methods?

Special methods (also called magic methods or dunder methods) start and end with double underscores (__). They allow you to customize how objects behave with built-in operations.

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
return f"Point({self.x}, {self.y})"

def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # Calls __add__
print(p3) # Calls __str__: Point(4, 6)

2. Object Initialization and Representation

2.1 Initialization Methods

class Person:
def __init__(self, name, age):
"""Constructor - called when creating instance"""
self.name = name
self.age = age

def __del__(self):
"""Destructor - called when object is garbage collected"""
print(f"{self.name} is being deleted")

person = Person("Alice", 25)
del person # Triggers __del__

2.2 String Representation

class Book:
def __init__(self, title, author):
self.title = title
self.author = author

def __str__(self):
"""User-friendly string"""
return f'"{self.title}" by {self.author}'

def __repr__(self):
"""Developer-friendly representation"""
return f'Book("{self.title}", "{self.author}")'

def __format__(self, format_spec):
"""Custom formatting"""
if format_spec == 'short':
return self.title
return str(self)

book = Book("1984", "George Orwell")
print(str(book)) # "1984" by George Orwell
print(repr(book)) # Book("1984", "George Orwell")
print(f"{book:short}") # 1984

3. Arithmetic Operators

3.1 Basic Arithmetic

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other):
"""Addition: v1 + v2"""
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
"""Subtraction: v1 - v2"""
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
"""Scalar multiplication: v * 2"""
return Vector(self.x * scalar, self.y * scalar)

def __truediv__(self, scalar):
"""Division: v / 2"""
return Vector(self.x / scalar, self.y / scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

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

print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(v1 / 2) # Vector(1.5, 2.0)

3.2 Augmented Assignment

class Counter:
def __init__(self, value=0):
self.value = value

def __iadd__(self, other):
"""In-place addition: c += 5"""
self.value += other
return self

def __isub__(self, other):
"""In-place subtraction: c -= 3"""
self.value -= other
return self

def __str__(self):
return str(self.value)

counter = Counter(10)
counter += 5
print(counter) # 15
counter -= 3
print(counter) # 12

4. Comparison Operators

4.1 Rich Comparison Methods

class Money:
def __init__(self, amount):
self.amount = amount

def __eq__(self, other):
"""Equal: =="""
return self.amount == other.amount

def __ne__(self, other):
"""Not equal: !="""
return self.amount != other.amount

def __lt__(self, other):
"""Less than: <"""
return self.amount < other.amount

def __le__(self, other):
"""Less than or equal: <="""
return self.amount <= other.amount

def __gt__(self, other):
"""Greater than: >"""
return self.amount > other.amount

def __ge__(self, other):
"""Greater than or equal: >="""
return self.amount >= other.amount

def __str__(self):
return f"${self.amount}"

m1 = Money(100)
m2 = Money(200)

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

5. Container Methods

5.1 Sequence Protocol

class CustomList:
def __init__(self, items=None):
self.items = items or []

def __len__(self):
"""Length: len(obj)"""
return len(self.items)

def __getitem__(self, index):
"""Get item: obj[index]"""
return self.items[index]

def __setitem__(self, index, value):
"""Set item: obj[index] = value"""
self.items[index] = value

def __delitem__(self, index):
"""Delete item: del obj[index]"""
del self.items[index]

def __contains__(self, item):
"""Membership: item in obj"""
return item in self.items

def __iter__(self):
"""Iteration: for item in obj"""
return iter(self.items)

clist = CustomList([1, 2, 3, 4, 5])
print(len(clist)) # 5
print(clist[2]) # 3
print(3 in clist) # True
clist[0] = 10
print(clist[0]) # 10

for item in clist:
print(item, end=' ') # 10 2 3 4 5

5.2 Custom Dictionary

class CaseInsensitiveDict:
def __init__(self):
self._data = {}

def __setitem__(self, key, value):
self._data[key.lower()] = value

def __getitem__(self, key):
return self._data[key.lower()]

def __delitem__(self, key):
del self._data[key.lower()]

def __contains__(self, key):
return key.lower() in self._data

def __len__(self):
return len(self._data)

d = CaseInsensitiveDict()
d["Name"] = "Alice"
d["AGE"] = 25

print(d["name"]) # Alice
print(d["Age"]) # 25
print("NAME" in d) # True

6. Callable Objects

6.1 Making Objects Callable

class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
"""Makes instance callable like a function"""
return x * self.factor

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

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

# Check if callable
print(callable(double)) # True

6.2 Function-like Classes

class Adder:
def __init__(self, start=0):
self.total = start

def __call__(self, value):
self.total += value
return self.total

adder = Adder(10)
print(adder(5)) # 15
print(adder(3)) # 18
print(adder(7)) # 25

7. Context Managers

7.1 Implementing enter and exit

class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
"""Called when entering 'with' block"""
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block"""
if self.file:
self.file.close()
# Return False to propagate exceptions
return False

with FileManager('test.txt', 'w') as f:
f.write('Hello, World!')
# File automatically closed

7.2 Database Connection Manager

class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None

def __enter__(self):
print(f"Connecting to {self.connection_string}")
self.connection = f"Connection to {self.connection_string}"
return self.connection

def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
self.connection = None
return False

with DatabaseConnection("localhost:5432") as conn:
print(f"Using {conn}")
# Do database operations
# Connection automatically closed

8. Attribute Access

8.1 Controlling Attribute Access

class DynamicAttributes:
def __init__(self):
self._data = {}

def __getattr__(self, name):
"""Called when attribute not found"""
return self._data.get(name, f"No attribute '{name}'")

def __setattr__(self, name, value):
"""Called when setting attribute"""
if name == '_data':
super().__setattr__(name, value)
else:
self._data[name] = value

def __delattr__(self, name):
"""Called when deleting attribute"""
if name in self._data:
del self._data[name]

obj = DynamicAttributes()
obj.name = "Alice"
obj.age = 25

print(obj.name) # Alice
print(obj.nonexistent) # No attribute 'nonexistent'
del obj.name
print(obj.name) # No attribute 'name'

9. Numeric Conversions

9.1 Type Conversion Methods

class Percentage:
def __init__(self, value):
self.value = value

def __int__(self):
"""Convert to int: int(obj)"""
return int(self.value)

def __float__(self):
"""Convert to float: float(obj)"""
return float(self.value)

def __str__(self):
return f"{self.value}%"

def __bool__(self):
"""Convert to bool: bool(obj)"""
return self.value > 0

p = Percentage(75.5)
print(int(p)) # 75
print(float(p)) # 75.5
print(bool(p)) # True

10. Summary

MethodOperationExample
__init__Constructorobj = Class()
__str__String representationstr(obj)
__repr__Developer representationrepr(obj)
__add__Additionobj1 + obj2
__sub__Subtractionobj1 - obj2
__mul__Multiplicationobj * 2
__eq__Equalityobj1 == obj2
__lt__Less thanobj1 < obj2
__len__Lengthlen(obj)
__getitem__Indexingobj[key]
__setitem__Assignmentobj[key] = value
__call__Call as functionobj()
__enter__/__exit__Context managerwith obj:
Key Takeaways
  • Special methods customize object behavior
  • Enable operator overloading
  • Make objects behave like built-in types
  • Follow Python conventions
  • Implement only needed methods

11. What's Next?

In Module 13 - Decorators & Closures, you'll learn:

  • Function decorators
  • Class decorators
  • Closures and scope
  • Built-in decorators
  • Creating custom decorators

12. Practice Exercises

Exercise 1: Complex Number Class

Implement a complex number class with arithmetic operations.

Exercise 2: Custom Range

Create a custom range class that mimics Python's range().

Exercise 3: Matrix Class

Build a matrix class with addition, multiplication, and indexing.

Exercise 4: Smart List

Create an enhanced list with additional methods and operator overloading.

Exercise 5: Timer Context Manager

Implement a context manager that times code execution.

Solutions

Try solving these exercises on your own first. Solutions will be provided in the practice section.