19.1 Introduction to Magic Methods
Magic methods are special methods whose names begin and end with two underscores. Python calls these methods automatically when an object is printed, compared, added, indexed, measured, or used in another built-in operation. Beginners usually do not call magic methods directly. Instead, they define them inside classes so their objects behave naturally.
Example
class Greeting:
def __str__(self):
return "Hello from a custom object"
message = Greeting()
print(message)
Output
Hello from a custom object
Output explanation: The print() function asks the object for a readable string. Python automatically calls __str__(), which returns the displayed sentence.
19.2 The Python Data Model
The Python data model is the collection of rules that explains how Python objects interact with the language. It defines what happens when you call functions such as len(), use operators such as +, access an item with brackets, or loop through an object. Magic methods connect custom classes to these standard Python operations.
Example
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
team = Team(["Ali", "Sara", "Mina"])
print(len(team))
Output
3
Output explanation: Calling len(team) activates the class's __len__() method. That method returns the number of names stored in the internal list.
19.3 __new__()
The __new__() method creates a new object before __init__() prepares it. It receives the class as its first argument and must return a new instance. Most beginner classes do not need to define __new__(), because Python already handles object creation. It is mainly useful for immutable types or advanced creation control.
Example
class Student:
def __new__(cls):
print("Creating the object")
return super().__new__(cls)
def __init__(self):
print("Initializing the object")
student = Student()
Output
Creating the object
Initializing the object
Output explanation: Python first calls __new__() to create the instance. After the instance exists, Python calls __init__() to prepare its starting data.
19.4 __init__()
The __init__() method initializes a newly created object. It is often called the constructor by beginners, although object creation technically happens in __new__(). You use __init__() to store starting values in instance attributes. Python runs it automatically whenever you create an object by calling the class name.
Example
class Book:
def __init__(self, title, price):
self.title = title
self.price = price
book = Book("Python Basics", 25)
print(book.title)
print(book.price)
Output
Python Basics
25
Output explanation: The values passed to Book() are stored in the new object's title and price attributes, and the two print statements display them.
19.5 __del__()
The __del__() method may run when an object is about to be destroyed. It can be used for simple cleanup messages, but it should not be trusted for important tasks because the exact destruction time is not always predictable. Safer cleanup is usually handled with context managers, especially for files, connections, and other resources.
Example
class Demo:
def __del__(self):
print("Object removed")
item = Demo()
del item
Output
Object removed
Output explanation: The del statement removes the variable reference. In this simple example, Python destroys the object and calls __del__(), which prints the message.
19.6 __str__()
The __str__() method returns a friendly description of an object for users. Python calls it when an object is passed to print() or str(). Without this method, printing a custom object usually shows a less helpful technical value. A good __str__() result should be clear, readable, and easy to understand.
Example
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name}: ${self.price}"
product = Product("Keyboard", 40)
print(product)
Output
Keyboard: $40
Output explanation: The object is printed directly. Python calls __str__(), which builds and returns a readable product description.
19.7 __repr__()
The __repr__() method returns a detailed representation mainly intended for programmers and debugging. A useful representation often shows the class name and the values needed to understand or recreate the object. Python uses it in the interactive console and when objects appear inside collections. It also becomes a fallback when __str__() is missing.
Example
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
user = User("Lina", 20)
print(repr(user))
Output
User(name='Lina', age=20)
Output explanation: The repr() function calls __repr__(). The returned text clearly shows the class and its stored values.
19.8 __len__()
The __len__() method lets a custom object work with Python's len() function. It must return a non-negative integer. The meaning of length depends on the class. For a playlist it may mean the number of songs, while for a shopping cart it may mean the number of products currently stored.
Example
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
playlist = Playlist(["Song A", "Song B", "Song C"])
print(len(playlist))
Output
3
Output explanation: The playlist contains three strings. The len() function calls the object's __len__() method and receives the number 3.
19.9 __getitem__()
The __getitem__() method controls what happens when square brackets are used to read an item from an object. The value inside the brackets is passed as the key or index. This method can make a custom class behave like a list, tuple, or dictionary. It can also support slicing when slice objects are handled correctly.
Example
class Menu:
def __init__(self, items):
self.items = items
def __getitem__(self, index):
return self.items[index]
menu = Menu(["Pizza", "Pasta", "Salad"])
print(menu[1])
Output
Pasta
Output explanation: The expression menu[1] calls __getitem__(1). The internal list item at index 1 is Pasta.
19.10 __setitem__()
The __setitem__() method controls assignments made with square brackets. Python calls it when code such as object[key] = value is used. This lets a custom object support list-like or dictionary-like updates. The method can directly store the value or validate it before changing the object's internal data.
Example
class Scores:
def __init__(self):
self.data = {}
def __setitem__(self, name, score):
self.data[name] = score
scores = Scores()
scores["Ali"] = 95
print(scores.data)
Output
{'Ali': 95}
Output explanation: The bracket assignment calls __setitem__(). It stores the key Ali and value 95 in the internal dictionary.
19.11 __delitem__()
The __delitem__() method runs when an item is deleted with bracket syntax, such as del object[key]. It gives the class control over how stored values are removed. A class can delete the item immediately, prevent deletion, print a message, or raise a helpful error when the requested key does not exist.
Example
class Basket:
def __init__(self):
self.items = ["Apple", "Bread", "Milk"]
def __delitem__(self, index):
del self.items[index]
basket = Basket()
del basket[1]
print(basket.items)
Output
['Apple', 'Milk']
Output explanation: The statement del basket[1] calls __delitem__(1). The item at index 1, which is Bread, is removed.
19.12 __contains__()
The __contains__() method defines how the in and not in operators check membership in a custom object. It should return True when the value exists and False otherwise. This method can search an internal collection or apply a custom rule, such as checking usernames without case sensitivity.
Example
class Library:
def __init__(self, books):
self.books = books
def __contains__(self, title):
return title in self.books
library = Library(["Python Basics", "Web Design"])
print("Python Basics" in library)
print("Java" in library)
Output
True
False
Output explanation: Python calls __contains__() for each membership test. The first title exists in the list, while the second title does not.
19.13 __iter__()
The __iter__() method makes an object iterable, meaning it can be used in a for loop. It must return an iterator. A simple class that already stores a list can return iter(self.items). More advanced classes may return themselves and then provide a separate __next__() method to produce values one at a time.
Example
class Colors:
def __init__(self):
self.items = ["Red", "Green", "Blue"]
def __iter__(self):
return iter(self.items)
colors = Colors()
for color in colors:
print(color)
Output
Red
Green
Blue
Output explanation: The loop calls __iter__(), which returns an iterator for the internal list. The loop then prints each color in order.
19.14 __next__()
The __next__() method returns the next value from an iterator. When no values remain, it must raise StopIteration. Python automatically handles that exception inside a for loop and stops looping. A custom iterator usually stores its current position as an attribute and increases that position after returning each value.
Example
class Counter:
def __init__(self, limit):
self.current = 1
self.limit = limit
def __iter__(self):
return self
def __next__(self):
if self.current > self.limit:
raise StopIteration
value = self.current
self.current += 1
return value
for number in Counter(3):
print(number)
Output
1
2
3
Output explanation: Each loop cycle calls __next__(). The method returns 1, 2, and 3, then raises StopIteration to end the loop.
19.15 Comparison Magic Methods
Comparison magic methods define how custom objects react to operators such as ==, !=, <, <=, >, and >=. Common methods include __eq__(), __lt__(), and __gt__(). They usually compare one important attribute, but they should first confirm that the other value is a compatible object.
Example
class Player:
def __init__(self, score):
self.score = score
def __gt__(self, other):
return self.score > other.score
player1 = Player(90)
player2 = Player(75)
print(player1 > player2)
Output
True
Output explanation: The > operator calls player1.__gt__(player2). Since 90 is greater than 75, the method returns True.
19.16 Arithmetic Magic Methods
Arithmetic magic methods let custom objects work with mathematical operators. Examples include __add__() for addition, __sub__() for subtraction, __mul__() for multiplication, and __truediv__() for division. These methods usually return a new object rather than changing the existing object, which makes their behavior similar to numbers and other built-in values.
Example
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __str__(self):
return f"${self.amount}"
first = Money(20)
second = Money(35)
print(first + second)
Output
$55
Output explanation: The + operator calls __add__(), which creates a new Money object containing 55. Printing that object calls __str__().
19.17 Callable Objects with __call__()
The __call__() method lets an object be used like a function. After defining it, you can place parentheses after an object and optionally pass arguments. Callable objects are useful when an object needs to remember information between calls. They combine stored state from a class with the convenient calling style of a normal function.
Example
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, number):
return number * self.factor
double = Multiplier(2)
print(double(6))
Output
12
Output explanation: The expression double(6) calls the object's __call__() method. It multiplies 6 by the stored factor 2.
19.18 Context Manager Methods
Context managers control setup and cleanup around a block of code used with the with statement. The __enter__() method runs when the block begins, and __exit__() runs when the block ends, even if an error occurs. They are commonly used for files, database connections, locks, and other resources that must be cleaned up safely.
Example
class StudySession:
def __enter__(self):
print("Session started")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Session ended")
with StudySession():
print("Learning Python")
Output
Session started
Learning Python
Session ended
Output explanation: Entering the with block calls __enter__(). After the body runs, Python calls __exit__() to complete cleanup.
19.19 Attribute Access Methods
Attribute access methods control how object attributes are read, assigned, or deleted. Important methods include __getattr__(), __getattribute__(), __setattr__(), and __delattr__(). These methods are powerful and should be used carefully because mistakes can cause endless recursion. A common beginner use is returning a helpful value when a missing attribute is requested.
Example
class Profile:
def __init__(self, name):
self.name = name
def __getattr__(self, attribute):
return f"{attribute} is not available"
profile = Profile("Sara")
print(profile.name)
print(profile.phone)
Output
Sara
phone is not available
Output explanation: The existing name attribute is returned normally. Because phone does not exist, Python calls __getattr__() and displays its message.
19.20 Creating Custom Python-Like Objects
A Python-like object supports familiar operations that match its purpose. For example, a custom collection may provide length, indexing, membership testing, iteration, and a readable string representation. You should not add magic methods only because they exist. Choose methods that make the class intuitive, predictable, and similar to related built-in Python objects.
Example
class TaskList:
def __init__(self, tasks):
self.tasks = tasks
def __len__(self):
return len(self.tasks)
def __getitem__(self, index):
return self.tasks[index]
def __contains__(self, task):
return task in self.tasks
def __str__(self):
return ", ".join(self.tasks)
tasks = TaskList(["Study", "Exercise"])
print(len(tasks))
print(tasks[0])
print("Exercise" in tasks)
print(tasks)
Output
2
Study
True
Study, Exercise
Output explanation: Different operations call different magic methods. The object supports length checking, indexing, membership testing, and readable printing like a built-in collection.
19.21 Chapter Practice Exercises
These exercises help you practise the most important magic methods from this chapter. Complete each task in a separate Python file, run it, and compare the result with what you expected. Try writing the solution yourself before reviewing earlier examples. Practice is important because magic methods become easier when you connect each method to a normal Python operation.
Example Exercise Solution
class Classroom:
def __init__(self, students):
self.students = students
def __len__(self):
return len(self.students)
def __contains__(self, name):
return name in self.students
classroom = Classroom(["Ali", "Mina", "Sara"])
print(len(classroom))
print("Mina" in classroom)
Output
3
True
Output explanation: The class contains three names, so len(classroom) returns 3. The membership test returns True because Mina is stored in the list.
Practice Tasks
- Create a
Movie class with readable __str__() and __repr__() methods.
- Create a
NumberBox class that supports addition with __add__().
- Create a collection class that supports
len(), indexing, and membership testing.
- Create an iterator that returns even numbers from 2 to 10.
- Create a callable object that adds a stored amount to a supplied number.
19.22 Chapter Mini Project
In this mini project, you will build a small shopping cart that behaves like a Python collection. It will support adding products, checking the number of products, reading products by index, checking whether a product exists, looping through products, and displaying a friendly summary. This project combines several magic methods in one practical class.
Mini Project: Python-Like Shopping Cart
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __contains__(self, item):
return item in self.items
def __iter__(self):
return iter(self.items)
def __str__(self):
if not self.items:
return "The cart is empty."
return "Cart: " + ", ".join(self.items)
cart = ShoppingCart()
cart.add("Bread")
cart.add("Milk")
cart.add("Apples")
print(cart)
print("Number of items:", len(cart))
print("First item:", cart[0])
print("Milk" in cart)
for item in cart:
print("-", item)
Output
Cart: Bread, Milk, Apples
Number of items: 3
First item: Bread
True
- Bread
- Milk
- Apples
Output explanation: The cart uses __str__() for its summary, __len__() for the item count, __getitem__() for indexing, __contains__() for the membership test, and __iter__() for the loop. Together, these methods make the custom cart feel like a normal Python collection.