Skip to main content

How to Define Special Methods (Dunder Methods) in Python Classes

Special methods, often called dunder methods (double underscore), are the mechanism Python uses to implement operator overloading and built-in function behavior for custom objects. By defining these methods, you can make your classes behave like standard types (e.g., using + to add objects, len() to get length, or print() to show a readable string).

This guide explains how to define the most essential special methods to create Pythonic classes.

Understanding Dunder Methods

Special methods are predefined methods starting and ending with double underscores (e.g., __init__). You do not call them directly (e.g., obj.__len__()). Instead, Python calls them when you use corresponding built-in functions (e.g., len(obj)).

Initialization (__init__)

The most common special method is __init__, which initializes a new instance of a class.

class Product:
def __init__(self, name, price):
self.name = name
self.price = price

p1 = Product("Laptop", 1000)
print(p1.name)

Output:

Laptop

String Representation (__str__ vs __repr__)

These methods control how your object looks when printed.

  • __str__: Should be readable for end-users (used by print()).
  • __repr__: Should be unambiguous for developers (used in the interactive shell or debugger).
class Product:
def __init__(self, name, price):
self.name = name
self.price = price

def __str__(self):
# Readable format
return f"{self.name} costs ${self.price}"

def __repr__(self):
# Developer format (ideally valid python code to recreate the object)
return f"Product(name='{self.name}', price={self.price})"

p = Product("Mouse", 25)
print(str(p)) # Calls __str__
print(repr(p)) # Calls __repr__

Output:

Mouse costs $25
Product(name='Mouse', price=25)

Operator Overloading (__add__, __eq__)

You can define how operators like +, -, ==, or < work with your objects.

Overloading Addition (+)

To allow obj1 + obj2, define __add__.

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

def __add__(self, other):
# Return a new Vector with summed coordinates
return Vector(self.x + other.x, self.y + other.y)

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

v1 = Vector(2, 3)
v2 = Vector(1, 4)
v3 = v1 + v2
print(v3)

Output:

Vector(3, 7)

Overloading Equality (==)

To allow custom comparison logic, define __eq__.

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

def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y

v1 = Vector(2, 3)
v2 = Vector(2, 3)
print(f"Are vectors equal? {v1 == v2}")

Output:

Are vectors equal? True

Container Emulation (__len__, __getitem__)

To make your class behave like a list or dictionary (indexing, iteration, length), implement container methods.

class Team:
def __init__(self, members):
self.members = members

def __len__(self):
# Called by len(team)
return len(self.members)

def __getitem__(self, index):
# Called by team[index]
return self.members[index]

team = Team(["Alice", "Bob", "Charlie"])

print(f"Team Size: {len(team)}")
print(f"First Member: {team[0]}")

# Since __getitem__ is defined, iteration works automatically!
for member in team:
print(f"- {member}")

Output:

Team Size: 3
First Member: Alice
- Alice
- Bob
- Charlie

Conclusion

Defining special methods makes your classes integrate seamlessly with Python's syntax.

  1. Always define __repr__ for debugging.
  2. Define __str__ if you need a pretty output for users.
  3. Implement __eq__ if you want to compare object values rather than memory identities.
  4. Use arithmetic methods (__add__, etc.) only when mathematical operations make logical sense for your object.