Python Special Methods are an important part of Object-Oriented Programming because they allow our custom objects to interact naturally with Python’s built-in operations and functions. These methods are also commonly called Dunder Methods.
The word dunder comes from double underscore. Python special methods usually begin and end with two underscores.
For example:
__init__()
__str__()
__len__()
__add__()
These methods are not normally called like ordinary methods in everyday Python programming. Instead, Python calls many of them automatically when we perform certain operations on an object.
For example, when we create an object:
student = Student("Rahul")
Python automatically calls the class’s:
__init__()
method.
Similarly, when we write:
print(student)
Python can use:
__str__()
to determine how the object should be represented as readable text.
When we write:
len(student)
Python can call:
__len__()
if the class provides that method.
Therefore, special methods provide a connection between our custom classes and Python’s built-in language features.
Special methods are predefined methods recognized by Python that allow objects to participate in specific language operations.
They are sometimes called:
For example:
__init__()
__str__()
__repr__()
__len__()
__add__()
__eq__()
Each method has a specific purpose.
Consider the following class:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
When we create:
student = Student("Rahul", 21)
Python automatically invokes:
student.__init__("Rahul", 21)
We normally do not write that call ourselves. Instead, we use the natural syntax:
Student("Rahul", 21)
This is one of the main purposes of special methods: they allow Python to provide convenient syntax for object behavior.
Python uses the double-underscore naming convention to identify special methods that have a specific meaning to the Python language.
For example:
__init__
__str__
__len__
__add__
These names are recognized by Python.
They are different from ordinary methods such as:
calculate_salary()
display_student()
generate_report()
An ordinary method is generally called explicitly:
student.display_student()
while special methods are often triggered by normal Python syntax.
For example:
len(student)
can trigger:
student.__len__()
Similarly:
student1 + student2
can trigger the appropriate addition special method.
This makes custom objects behave more like Python’s built-in objects.
The most commonly encountered special method in Python OOP is:
__init__()
It is commonly used to initialize an object’s attributes when the object is created.
For example:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
Now create an object:
student = Student("Rahul", 21)
Python automatically invokes the initialization method.
The values are assigned to:
self.name
self.age
We can access them:
print(student.name)
print(student.age)
Output:
Rahul
21
Without __init__(), we could still create attributes later, but it would be less convenient and less structured.
Consider a simple employee class:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
Create two objects:
employee1 = Employee(
"Rahul",
50000
)
employee2 = Employee(
"Priya",
60000
)
Each object receives its own values.
print(employee1.name)
print(employee1.salary)
print(employee2.name)
print(employee2.salary)
Output:
Rahul
50000
Priya
60000
The same __init__() method is used to initialize both objects, but each object stores different data.
One of the important purposes of __init__() is establishing the initial state of an object.
For example:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
When we create:
account = BankAccount(
"ACC1001",
50000
)
the object starts with a defined state:
account_number = "ACC1001"
balance = 50000
Other methods can then operate on that state.
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
Now:
account = BankAccount(
"ACC1001",
50000
)
account.deposit(10000)
print(account.balance)
Output:
60000
The __init__() method established the initial state, while normal methods modified that state.
A common beginner explanation is that __init__() is the constructor. In practical Python teaching, it is often described this way because it initializes a newly created object.
Technically, Python separates object creation from object initialization.
The special method responsible for creating an instance is:
__new__()
while:
__init__()
initializes the already-created instance.
For most beginner and intermediate Python OOP programs, you will primarily work with __init__(). The distinction becomes important when studying advanced object creation and customization.
We can also provide default values.
class Student:
def __init__(
self,
name,
age=18
):
self.name = name
self.age = age
Now:
student1 = Student("Rahul", 21)
student2 = Student("Priya")
For student1, the age is explicitly provided.
For student2, Python uses the default value:
18
Another extremely useful special method is:
__str__()
It controls the human-readable string representation of an object.
Consider:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
student = Student(
"Rahul",
21
)
print(student)
Without defining __str__(), Python generally displays a default object representation that is not very useful to a normal user.
We can improve this by defining:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return (
f"Student: {self.name}, "
f"Age: {self.age}"
)
Now:
student = Student(
"Rahul",
21
)
print(student)
Output:
Student: Rahul, Age: 21
This is much more readable.
When we write:
print(student)
Python can use the object’s __str__() method to determine what text should be displayed.
Conceptually:
print(student)
↓
student.__str__()
↓
Readable string
This allows our classes to define their own human-readable representation.
A common mistake is returning a number or another data type from __str__().
Incorrect:
def __str__(self):
return self.age
If self.age is an integer, this causes an error because __str__() must return a string.
Correct:
def __str__(self):
return str(self.age)
Or, more usefully:
def __str__(self):
return f"Age: {self.age}"
Suppose we create a class representing a dataset.
class Dataset:
def __init__(
self,
name,
rows,
columns
):
self.name = name
self.rows = rows
self.columns = columns
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Rows: {self.rows}, "
f"Columns: {self.columns}"
)
Create an object:
dataset = Dataset(
"Sales Data",
50000,
12
)
Now:
print(dataset)
Output:
Dataset: Sales Data, Rows: 50000, Columns: 12
This is particularly useful when working with custom Data Analytics classes because it makes debugging and displaying object information easier.
Special methods allow custom classes to integrate with Python’s syntax and built-in functionality.
Without special methods, a custom object might behave like an isolated structure that only supports explicitly defined methods.
With special methods, we can make an object behave naturally.
For example:
len(object)
str(object)
object1 + object2
object1 == object2
can all be customized through appropriate special methods.
This means Python’s object model is highly customizable.
Python provides many special methods. Some important ones are:
| Special Method | Purpose |
|---|---|
__init__() |
Initialize an object |
__str__() |
Human-readable string representation |
__repr__() |
Developer-oriented representation |
__len__() |
Defines behavior for len() |
__add__() |
Defines behavior for + |
__eq__() |
Defines equality comparison |
__lt__() |
Defines less-than comparison |
__gt__() |
Defines greater-than comparison |
We will explore these methods in more detail later in the lesson.
Special methods are part of Python’s data model. They allow objects to interact with Python’s core language features in a standardized way.
For example, when Python encounters:
a + b
it looks for the appropriate addition behavior.
When Python encounters:
len(a)
it looks for the object’s length behavior.
When Python encounters:
a == b
it can use the object’s equality behavior.
Therefore, special methods act as a bridge between:
Python Syntax
↓
Special Method
↓
Object Behavior
This is why learning dunder methods is an important step toward understanding advanced Python OOP.
__init__() initializes an object’s state.__str__() provides a human-readable representation of an object.__new__() is involved in object creation, while __init__() initializes the created object.In the next section, we will explore more important dunder methods including __repr__(), __len__(), __add__(), __eq__(), __lt__(), and __gt__(), with practical examples.
Python provides many special methods that allow custom objects to work naturally with built-in Python operations. In the previous section, we introduced __init__() and __str__(). Now we will explore several other important dunder methods that are commonly used in Python OOP.
The most useful methods in this section are:
__repr__()__len__()__add__()__eq__()__lt__()__gt__()These methods allow our custom objects to participate in operations such as displaying objects, calculating lengths, addition, equality comparisons, and sorting.
The __repr__() method provides a representation of an object that is primarily intended for developers and debugging.
Consider:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
We can add __repr__():
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return (
f"Student("
f"name='{self.name}', "
f"age={self.age})"
)
Now:
student = Student("Rahul", 21)
print(repr(student))
Output:
Student(name='Rahul', age=21)
This representation provides useful information about the object.
The key difference is that __str__() is generally designed for a user-friendly representation, while __repr__() is generally designed for developers, debugging, and inspecting objects.
| Method | Main Purpose |
|---|---|
__str__() |
Readable representation for users |
__repr__() |
Detailed representation for developers |
For example:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name}: ₹{self.price}"
def __repr__(self):
return (
f"Product("
f"name='{self.name}', "
f"price={self.price})"
)
Now:
product = Product(
"Laptop",
50000
)
Using:
print(product)
may produce:
Laptop: ₹50000
while:
print(repr(product))
produces:
Product(name='Laptop', price=50000)
The two methods serve different purposes.
The __len__() method allows an object to work with Python’s built-in len() function.
Consider a simple class representing a course:
class Course:
def __init__(self, lessons):
self.lessons = lessons
def __len__(self):
return len(self.lessons)
Create an object:
course = Course([
"Python",
"SQL",
"Excel",
"Power BI"
])
Now:
print(len(course))
Output:
4
Python internally uses the object’s length behavior.
Conceptually:
len(course)
↓
course.__len__()
↓
4
This allows custom objects to behave like collections.
Suppose we create a custom dataset object:
class Dataset:
def __init__(self, records):
self.records = records
def __len__(self):
return len(self.records)
Create a dataset:
dataset = Dataset([
{"id": 1, "sales": 500},
{"id": 2, "sales": 700},
{"id": 3, "sales": 900}
])
Now:
print(len(dataset))
Output:
3
This provides a natural way to ask a custom dataset object how many records it contains.
The __add__() method defines how the + operator behaves for custom objects.
Consider:
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Number(
self.value + other.value
)
Create two objects:
num1 = Number(10)
num2 = Number(20)
Now:
result = num1 + num2
Python can internally perform behavior equivalent to:
num1.__add__(num2)
We can check the result:
print(result.value)
Output:
30
This is called operator overloading.
Let’s create a practical example.
class Cart:
def __init__(self, total):
self.total = total
def __add__(self, other):
return Cart(
self.total + other.total
)
Create two carts:
cart1 = Cart(1500)
cart2 = Cart(2500)
Add them:
total_cart = cart1 + cart2
Then:
print(total_cart.total)
Output:
4000
The + operator has been customized for the Cart class.
Suppose we have two data summaries containing record counts.
class DataSummary:
def __init__(self, records):
self.records = records
def __add__(self, other):
return DataSummary(
self.records + other.records
)
Create two summaries:
sales_data = DataSummary(5000)
customer_data = DataSummary(3000)
Now:
combined = sales_data + customer_data
Then:
print(combined.records)
Output:
8000
This demonstrates how operator overloading can make custom analytical objects easier to work with.
The __eq__() method defines how two objects should be compared using:
==
Consider:
class Student:
def __init__(self, student_id):
self.student_id = student_id
def __eq__(self, other):
return (
self.student_id
== other.student_id
)
Create two objects:
student1 = Student(101)
student2 = Student(101)
Now:
print(student1 == student2)
Output:
True
Why?
Because both objects have the same student_id.
Python uses the equality method to determine how these objects should be compared.
Consider:
student1 = Student(101)
student2 = Student(102)
Now:
print(student1 == student2)
Output:
False
The custom equality rule determines the result.
Suppose we have a dataset configuration object.
class DatasetConfig:
def __init__(self, name, version):
self.name = name
self.version = version
def __eq__(self, other):
return (
self.name == other.name
and
self.version == other.version
)
Now:
config1 = DatasetConfig(
"Sales",
1
)
config2 = DatasetConfig(
"Sales",
1
)
Then:
print(config1 == config2)
Output:
True
This can be useful when determining whether two configuration or analytical objects represent the same logical state.
The __lt__() method defines the behavior of the less-than operator:
<
For example:
class Employee:
def __init__(self, salary):
self.salary = salary
def __lt__(self, other):
return self.salary < other.salary
Create two employees:
employee1 = Employee(50000)
employee2 = Employee(70000)
Now:
print(employee1 < employee2)
Output:
True
Python uses:
employee1.__lt__(employee2)
to determine the comparison.
The __gt__() method defines the behavior of:
>
For example:
class Employee:
def __init__(self, salary):
self.salary = salary
def __gt__(self, other):
return self.salary > other.salary
Now:
employee1 = Employee(70000)
employee2 = Employee(50000)
print(employee1 > employee2)
Output:
True
Comparison methods become particularly useful when objects need to be ordered.
For example:
class Student:
def __init__(self, marks):
self.marks = marks
def __lt__(self, other):
return self.marks < other.marks
Create students:
students = [
Student(85),
Student(60),
Student(95),
Student(72)
]
Python can use comparison behavior when sorting compatible objects:
students.sort()
The objects can then be processed in ascending order based on their marks.
Operator overloading allows code involving custom objects to look natural.
Without operator overloading, we might have to write:
total = cart1.add(cart2)
With __add__(), we can write:
total = cart1 + cart2
Similarly, instead of:
student1.is_equal(student2)
we can define __eq__() and write:
student1 == student2
This makes custom objects feel more like built-in Python objects.
| Operator | Special Method |
|---|---|
== |
__eq__() |
!= |
__ne__() |
< |
__lt__() |
<= |
__le__() |
> |
__gt__() |
>= |
__ge__() |
| Operator | Special Method |
|---|---|
+ |
__add__() |
- |
__sub__() |
* |
__mul__() |
/ |
__truediv__() |
// |
__floordiv__() |
% |
__mod__() |
** |
__pow__() |
Python provides many more special methods, but these are some of the most important ones for understanding operator overloading.
Many Python built-in operations are connected to special methods.
| Python Operation | Related Special Method |
|---|---|
str(obj) |
__str__() |
repr(obj) |
__repr__() |
len(obj) |
__len__() |
obj1 + obj2 |
__add__() |
obj1 == obj2 |
__eq__() |
obj1 < obj2 |
__lt__() |
obj1 > obj2 |
__gt__() |
This relationship is the key to understanding how Python’s object model works.
__repr__() provides a developer-oriented representation of an object.__len__() allows custom objects to work with len().__add__() defines the behavior of the + operator.__eq__() defines equality comparison.__lt__() defines less-than comparison.__gt__() defines greater-than comparison.In the next section, we will combine these special methods into practical Python OOP applications, including custom collections, object comparison, operator overloading, and Data Analytics-oriented examples.
Special methods become especially powerful when they are used to make custom classes behave naturally with Python’s built-in syntax. Instead of creating separate methods for every operation, we can implement appropriate dunder methods and allow Python to use familiar expressions such as len(), +, ==, and < with our own objects.
This is particularly useful when designing reusable Python applications, data-processing systems, reporting tools, and Data Analytics projects.
Let’s create a practical Dataset class.
class Dataset:
def __init__(self, name, records):
self.name = name
self.records = records
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Records: {len(self.records)}"
)
def __len__(self):
return len(self.records)
Create a dataset:
sales = Dataset(
"Sales Data",
[
100,
200,
300,
400
]
)
Now we can use:
print(sales)
Output:
Dataset: Sales Data, Records: 4
And:
print(len(sales))
Output:
4
Our custom object now behaves naturally with both print() and len().
Suppose we want to combine two datasets.
We can define __add__():
class Dataset:
def __init__(self, name, records):
self.name = name
self.records = records
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Records: {len(self.records)}"
)
def __len__(self):
return len(self.records)
def __add__(self, other):
return Dataset(
self.name + " + " + other.name,
self.records + other.records
)
Create two datasets:
sales_2025 = Dataset(
"Sales 2025",
[100, 200, 300]
)
sales_2026 = Dataset(
"Sales 2026",
[400, 500]
)
Now:
combined = sales_2025 + sales_2026
Print the result:
print(combined)
Output:
Dataset: Sales 2025 + Sales 2026, Records: 5
The expression:
sales_2025 + sales_2026
is made possible by:
__add__()
Suppose we want to compare datasets according to the number of records.
We can implement __gt__():
class Dataset:
def __init__(self, name, records):
self.name = name
self.records = records
def __len__(self):
return len(self.records)
def __gt__(self, other):
return len(self) > len(other)
Create two datasets:
dataset1 = Dataset(
"Dataset A",
[1, 2, 3, 4, 5]
)
dataset2 = Dataset(
"Dataset B",
[1, 2]
)
Now:
print(dataset1 > dataset2)
Output:
True
The comparison is based on the number of records rather than the object’s memory identity.
We can also define when two datasets should be considered equal.
class Dataset:
def __init__(self, name, records):
self.name = name
self.records = records
def __eq__(self, other):
return (
self.name == other.name
and
self.records == other.records
)
Now:
dataset1 = Dataset(
"Sales",
[100, 200]
)
dataset2 = Dataset(
"Sales",
[100, 200]
)
print(dataset1 == dataset2)
Output:
True
Without defining the appropriate equality behavior, two separate objects generally do not become equal simply because they contain similar data.
Let’s look at a business example.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return (
f"{self.name}: ₹{self.price}"
)
def __lt__(self, other):
return self.price < other.price
def __gt__(self, other):
return self.price > other.price
Create products:
laptop = Product(
"Laptop",
50000
)
phone = Product(
"Phone",
30000
)
Now:
print(laptop)
print(phone)
Output:
Laptop: ₹50000
Phone: ₹30000
We can also compare them:
print(laptop > phone)
Output:
True
This makes the custom class easier to work with.
Comparison methods become particularly useful when we want to sort objects.
Consider:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def __lt__(self, other):
return self.salary < other.salary
Create several employees:
employees = [
Employee("Rahul", 60000),
Employee("Priya", 45000),
Employee("Amit", 75000),
Employee("Neha", 55000)
]
We can sort them:
employees.sort()
The comparison behavior defined by __lt__() allows Python to determine their order.
We can then display them:
for employee in employees:
print(
employee.name,
employee.salary
)
The employees will be ordered according to salary.
Let’s build another practical example.
class Cart:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
def __str__(self):
return (
f"Shopping Cart: "
f"{len(self.items)} items"
)
Create:
cart = Cart([
"Laptop",
"Mouse",
"Keyboard"
])
Now:
print(cart)
Output:
Shopping Cart: 3 items
And:
print(len(cart))
Output:
3
The class provides natural behavior through special methods.
We can combine several special methods in one class.
class DataBatch:
def __init__(self, name, records):
self.name = name
self.records = records
def __str__(self):
return (
f"{self.name}: "
f"{len(self.records)} records"
)
def __len__(self):
return len(self.records)
def __add__(self, other):
return DataBatch(
self.name + " + " + other.name,
self.records + other.records
)
Create two batches:
batch1 = DataBatch(
"January",
[10, 20, 30]
)
batch2 = DataBatch(
"February",
[40, 50]
)
Display them:
print(batch1)
print(batch2)
Output:
January: 3 records
February: 2 records
Combine them:
combined = batch1 + batch2
Then:
print(combined)
print(len(combined))
Output:
January + February: 5 records
5
This is a good example of how several dunder methods can work together.
One of the main advantages of special methods is that they allow custom objects to work with Python’s existing functions.
For example:
str(obj)
can use:
__str__()
Similarly:
len(obj)
can use:
__len__()
And:
obj1 + obj2
can use:
__add__()
This means that developers do not need to create a separate syntax for every custom class.
Instead, the class can integrate with Python’s existing language features.
Special methods can make Data Analytics classes much easier to use.
Imagine a custom class representing a collection of observations:
class DataCollection:
def __init__(self, values):
self.values = values
def __len__(self):
return len(self.values)
def __str__(self):
return (
f"DataCollection("
f"count={len(self)})"
)
def __add__(self, other):
return DataCollection(
self.values + other.values
)
Create:
data1 = DataCollection(
[10, 20, 30]
)
data2 = DataCollection(
[40, 50, 60]
)
Now:
combined = data1 + data2
We can use:
print(combined)
print(len(combined))
Output:
DataCollection(count=6)
6
This style can make custom analytical classes easier to read and use.
__repr__() becomes particularly useful when objects are stored inside collections.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return (
f"Product("
f"'{self.name}', "
f"{self.price})"
)
Create a list:
products = [
Product("Laptop", 50000),
Product("Mouse", 1000),
Product("Keyboard", 2000)
]
Now:
print(products)
The representation of each object can be shown in a useful developer-friendly format.
This is one reason __repr__() is valuable during debugging and development.
Special methods should make intuitive sense.
For example, if a class represents two numerical values, using:
object1 + object2
to combine those values may be logical.
But defining + to delete an object would be confusing.
Good operator overloading should make code:
A useful rule is:
Use familiar operators for intuitive operations.
| Normal Method | Special Method |
|---|---|
| Usually called explicitly | Often triggered by Python syntax |
Example: calculate() |
Example: __add__() |
| Developer chooses the method name | Python defines the recognized method name |
| Provides application-specific behavior | Connects objects to Python’s data model |
For example:
employee.calculate_salary()
is an explicit method call.
While:
employee1 == employee2
can invoke:
__eq__()
automatically.
| Method | Use |
|---|---|
__init__() |
Initialize object |
__str__() |
User-friendly representation |
__repr__() |
Developer-friendly representation |
__len__() |
Object length |
__add__() |
Addition |
__eq__() |
Equality |
__lt__() |
Less-than comparison |
__gt__() |
Greater-than comparison |
__str__() makes objects easier to display.__repr__() provides useful developer-oriented representations.__len__() allows objects to work with len().__add__() enables custom addition behavior.__eq__(), __lt__(), and __gt__() allow custom comparisons.In the final section, we will build a complete project using multiple dunder methods, followed by coding exercises, common mistakes, interview questions, and a complete revision of Python Special Methods.
In this final section, we will combine the important special methods learned throughout this lesson into a practical Python OOP project. The goal is not only to memorize dunder methods but to understand when and why they are useful.
We will build a small Data Analytics Dataset Manager that can represent datasets, display them, count records, combine datasets, compare datasets, and provide useful developer-friendly representations.
First, import nothing because the special methods we need are already part of Python’s object model.
We will create a Dataset class with the following attributes:
Our class will implement several special methods:
__init__()
__str__()
__repr__()
__len__()
__add__()
__eq__()
__lt__()
Let’s start with the class:
class Dataset:
def __init__(
self,
name,
records,
source
):
self.name = name
self.records = records
self.source = source
The __init__() method initializes the object.
Now we can create a dataset:
sales = Dataset(
"Sales Data",
[100, 200, 300],
"CSV"
)
The object now contains:
name = "Sales Data"
records = [100, 200, 300]
source = "CSV"
Now let’s make the object readable when we use print().
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Source: {self.source}, "
f"Records: {len(self.records)}"
)
The complete class now becomes:
class Dataset:
def __init__(
self,
name,
records,
source
):
self.name = name
self.records = records
self.source = source
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Source: {self.source}, "
f"Records: {len(self.records)}"
)
Now:
print(sales)
produces:
Dataset: Sales Data, Source: CSV, Records: 3
Instead of seeing an unhelpful default object representation, we get useful information.
Now let’s add a developer-friendly representation.
def __repr__(self):
return (
f"Dataset("
f"name='{self.name}', "
f"records={self.records}, "
f"source='{self.source}')"
)
The complete class now contains both representations:
class Dataset:
def __init__(
self,
name,
records,
source
):
self.name = name
self.records = records
self.source = source
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Source: {self.source}, "
f"Records: {len(self.records)}"
)
def __repr__(self):
return (
f"Dataset("
f"name='{self.name}', "
f"records={self.records}, "
f"source='{self.source}')"
)
Now:
print(sales)
is intended to provide a user-friendly representation.
While:
print(repr(sales))
provides a more detailed developer-oriented representation.
Now we want the following expression to work:
len(sales)
Add:
def __len__(self):
return len(self.records)
Now:
print(len(sales))
produces:
3
The custom object now behaves similarly to a collection.
Suppose we have sales data from two different months.
january = Dataset(
"January Sales",
[100, 200, 300],
"CSV"
)
february = Dataset(
"February Sales",
[400, 500],
"Excel"
)
We want to combine them using:
combined = january + february
To support this operation, define:
def __add__(self, other):
return Dataset(
self.name + " + " + other.name,
self.records + other.records,
"Combined"
)
Now the addition operation works naturally.
combined = january + february
print(combined)
Output:
Dataset: January Sales + February Sales,
Source: Combined,
Records: 5
The expression:
january + february
uses the custom __add__() implementation.
Now suppose we want to determine whether two datasets contain the same logical information.
We can define equality based on the dataset name and records.
def __eq__(self, other):
return (
self.name == other.name
and
self.records == other.records
)
Create two datasets:
dataset1 = Dataset(
"Sales",
[100, 200, 300],
"CSV"
)
dataset2 = Dataset(
"Sales",
[100, 200, 300],
"Excel"
)
Now:
print(dataset1 == dataset2)
The result is:
True
because our equality rule only compares the name and records, not the source.
If the source should also matter, we could include it in the equality condition.
Now let’s allow datasets to be compared according to their number of records.
def __lt__(self, other):
return len(self) < len(other)
Now create:
small = Dataset(
"Small Dataset",
[1, 2],
"CSV"
)
large = Dataset(
"Large Dataset",
[1, 2, 3, 4, 5],
"CSV"
)
Now:
print(small < large)
returns:
True
because the small dataset contains fewer records.
We can now combine all the methods into one class:
class Dataset:
def __init__(
self,
name,
records,
source
):
self.name = name
self.records = records
self.source = source
def __str__(self):
return (
f"Dataset: {self.name}, "
f"Source: {self.source}, "
f"Records: {len(self.records)}"
)
def __repr__(self):
return (
f"Dataset("
f"name='{self.name}', "
f"records={self.records}, "
f"source='{self.source}')"
)
def __len__(self):
return len(self.records)
def __add__(self, other):
return Dataset(
self.name + " + " + other.name,
self.records + other.records,
"Combined"
)
def __eq__(self, other):
return (
self.name == other.name
and
self.records == other.records
)
def __lt__(self, other):
return len(self) < len(other)
Now our custom object supports:
print(dataset)
repr(dataset)
len(dataset)
dataset1 + dataset2
dataset1 == dataset2
dataset1 < dataset2
This demonstrates how special methods allow custom objects to integrate with Python’s language features.
Let’s create several datasets:
sales = Dataset(
"Sales",
[100, 200, 300, 400],
"CSV"
)
customers = Dataset(
"Customers",
[1, 2, 3],
"Excel"
)
products = Dataset(
"Products",
[10, 20, 30, 40, 50],
"MySQL"
)
Display them:
print(sales)
print(customers)
print(products)
Find their sizes:
print(len(sales))
print(len(customers))
print(len(products))
Compare datasets:
print(sales > customers)
Combine compatible datasets:
combined = sales + customers
print(combined)
The exact behavior is controlled by our special methods.
Create a Student class with:
name
marks
Implement:
__init__()
__str__()
__eq__()
__lt__()
The class should allow:
print(student)
student1 == student2
student1 < student2
Use marks for comparison.
Create a ShoppingCart class containing a list of products.
Implement:
__init__()
__len__()
__str__()
__add__()
The following should work:
len(cart)
print(cart)
combined_cart = cart1 + cart2
Create an Employee class containing:
name
salary
Implement:
__str__()
__repr__()
__eq__()
__lt__()
__gt__()
Allow employees to be compared according to salary.
Create:
DataCollection
with a list of numerical values.
Implement:
__len__()
__str__()
__add__()
Adding two collections should create a new collection containing the values from both.
Create a Report class with:
title
records
Implement:
__str__()
__repr__()
__len__()
__eq__()
Two reports should be considered equal when their titles and records are the same.
Mistake 1: Returning the wrong type from __str__()
__str__() must return a string.
Incorrect:
def __str__(self):
return self.price
Correct:
def __str__(self):
return str(self.price)
Mistake 2: Forgetting the return value
Methods such as __len__() must return an appropriate integer.
Incorrect:
def __len__(self):
print(len(self.records))
Correct:
def __len__(self):
return len(self.records)
Mistake 3: Assuming __add__() must modify the original object
It does not have to. It can return a new object, as we did with our Dataset class.
Mistake 4: Making operators behave unexpectedly
Operator overloading should be intuitive. If a developer sees:
a + b
the operation should have a reasonable meaning.
Mistake 5: Confusing __str__() and __repr__()
Use __str__() primarily for readable output and __repr__() primarily for detailed developer-oriented representation.
1. What are dunder methods?
Dunder methods are special Python methods whose names generally begin and end with double underscores. They allow objects to interact with Python’s built-in operations and syntax.
2. Why are they called dunder methods?
Dunder is short for double underscore.
3. What does __init__() do?
It initializes an object’s state after the object has been created.
4. What is __str__() used for?
It provides a human-readable string representation of an object.
5. What is __repr__() used for?
It provides a developer-oriented representation that is useful for debugging and inspecting objects.
6. What does __len__() do?
It defines the behavior of the built-in len() function for an object.
7. What does __add__() do?
It defines the behavior of the + operator for custom objects.
8. What is operator overloading?
Operator overloading allows operators such as +, ==, and < to behave appropriately with custom objects.
9. What does __eq__() do?
It defines equality comparison using the == operator.
10. What is __new__()?
__new__() is involved in creating a new instance, while __init__() initializes that instance.
| Operation | Dunder Method | Purpose |
|---|---|---|
| Object initialization | __init__() |
Initialize object state |
| Readable output | __str__() |
User-friendly representation |
| Developer representation | __repr__() |
Debugging and inspection |
len(obj) |
__len__() |
Define object length |
obj1 + obj2 |
__add__() |
Custom addition |
obj1 == obj2 |
__eq__() |
Equality comparison |
obj1 < obj2 |
__lt__() |
Less-than comparison |
obj1 > obj2 |
__gt__() |
Greater-than comparison |
Python special methods are an essential part of Python’s object model. They allow developers to define how custom objects behave when Python performs built-in operations.
The most important concept is that special methods connect familiar Python syntax to custom object behavior.
print(obj)
↓
__str__()
len(obj)
↓
__len__()
obj1 + obj2
↓
__add__()
obj1 == obj2
↓
__eq__()
obj1 < obj2
↓
__lt__()
Once you understand this relationship, dunder methods become much easier to remember.
Instead of memorizing isolated method names, think about the operation you want your object to support.
For example:
__str__().__repr__().len() to work? Use __len__().+ to work? Use __add__().== to work? Use __eq__().Special methods therefore make custom Python classes feel more natural, expressive, and integrated with the language itself.
After completing this lesson, you should be able to identify common dunder methods, explain their purpose, implement them in your own classes, and use operator overloading to create more natural Python objects.