Lesson 4 - Special Methods and Python's Data Model
Introduction
You’ve already used special methods without realizing it—__init__ is one! Special methods (also called magic methods or dunder methods because of the double underscores) let you customize how your objects work with Python’s built-in operations.
Want to use + to combine objects? Define __add__. Want to use len() on your object? Define __len__. Want to make your object sortable? Define __lt__.
These methods are part of Python’s data model—the framework that makes Python feel consistent and intuitive. In this lesson, we’ll learn:
- String representation methods:
__str__and__repr__ - Comparison methods:
__eq__,__lt__,__le__,__gt__,__ge__ - Container methods:
__len__,__getitem__,__setitem__,__contains__ - Arithmetic methods:
__add__,__sub__, etc. - Other useful special methods
String Representation: __str__ and __repr__
When you print an object or use it in a string context, Python calls special methods to get a string representation.
Without Special Methods
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
book = Book("Python Crash Course", "Eric Matthes", 39.99)
print(book)Output:
<__main__.Book object at 0x7f8b1c3e4a90>Not very helpful! Let’s fix that with __str__ and __repr__:
__str__: User-Friendly String
The __str__ method returns a human-readable string representation. It’s called by print() and str():
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __str__(self):
return f"'{self.title}' by {self.author} - ${self.price}"
book = Book("Python Crash Course", "Eric Matthes", 39.99)
print(book) # Calls __str__
print(str(book)) # Explicitly calls __str__Output:
'Python Crash Course' by Eric Matthes - $39.99
'Python Crash Course' by Eric Matthes - $39.99__repr__: Developer-Friendly String
The __repr__ method returns a detailed, unambiguous representation. It should ideally be valid Python code that could recreate the object:
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __str__(self):
return f"'{self.title}' by {self.author}"
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.price})"
book = Book("Python Crash Course", "Eric Matthes", 39.99)
print(str(book)) # User-friendly
print(repr(book)) # Developer-friendly
# repr() is also used in lists
books = [book]
print(books) # Uses __repr__Output:
'Python Crash Course' by Eric Matthes
Book('Python Crash Course', 'Eric Matthes', 39.99)
[Book('Python Crash Course', 'Eric Matthes', 39.99)]Best Practice: Always implement __repr__. Implement __str__ only if you need a different user-facing representation.
Comparison Methods
Comparison methods let you use operators like ==, <, > with your objects.
__eq__: Equality (==)
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __eq__(self, other):
"""Two books are equal if they have the same title and author"""
if not isinstance(other, Book):
return False
return self.title == other.title and self.author == other.author
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.price})"
book1 = Book("Python Basics", "John Doe", 29.99)
book2 = Book("Python Basics", "John Doe", 34.99) # Different price
book3 = Book("Python Advanced", "John Doe", 29.99)
print(book1 == book2) # True (same title and author)
print(book1 == book3) # False (different title)Output:
True
FalseOrdering Methods: __lt__, __le__, __gt__, __ge__
These methods enable sorting and ordering:
__lt__: less than (<)__le__: less than or equal (<=)__gt__: greater than (>)__ge__: greater than or equal (>=)
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __lt__(self, other):
"""Compare books by price"""
return self.price < other.price
def __le__(self, other):
return self.price <= other.price
def __gt__(self, other):
return self.price > other.price
def __ge__(self, other):
return self.price >= other.price
def __eq__(self, other):
return self.price == other.price
def __repr__(self):
return f"Book('{self.title}', ${self.price})"
books = [
Book("Python Advanced", "Jane Smith", 49.99),
Book("Python Basics", "John Doe", 29.99),
Book("Python Intermediate", "Bob Johnson", 39.99),
]
# Now we can sort books!
sorted_books = sorted(books)
print("Books sorted by price:")
for book in sorted_books:
print(f" {book}")
# And use comparison operators
book1 = books[0]
book2 = books[1]
print(f"\n{book1.title} costs more than {book2.title}: {book1 > book2}")Output:
Books sorted by price:
Book('Python Basics', $29.99)
Book('Python Intermediate', $39.99)
Book('Python Advanced', $49.99)
Python Advanced costs more than Python Basics: TrueShortcut: Instead of implementing all comparison methods, you can use functools.total_ordering:
from functools import total_ordering
@total_ordering
class Book:
def __init__(self, title, price):
self.title = title
self.price = price
def __eq__(self, other):
return self.price == other.price
def __lt__(self, other):
return self.price < other.price
# Python automatically generates __le__, __gt__, __ge__!
def __repr__(self):
return f"Book('{self.title}', ${self.price})"
book1 = Book("Cheap Book", 19.99)
book2 = Book("Expensive Book", 49.99)
print(book1 < book2) # True
print(book1 <= book2) # True (auto-generated)
print(book1 > book2) # False (auto-generated)
print(book1 >= book2) # False (auto-generated)Container Methods
These methods make your objects behave like containers (lists, dictionaries, etc.).
__len__: Length
class BookCollection:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def __len__(self):
return len(self.books)
collection = BookCollection()
collection.add_book("Python Basics")
collection.add_book("Python Advanced")
print(len(collection)) # 2__getitem__: Index Access
class BookCollection:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def __len__(self):
return len(self.books)
def __getitem__(self, index):
"""Allow indexing: collection[0]"""
return self.books[index]
collection = BookCollection()
collection.add_book("Python Basics")
collection.add_book("Python Advanced")
collection.add_book("Python Expert")
print(collection[0]) # Python Basics
print(collection[1]) # Python Advanced
print(collection[-1]) # Python Expert
# Bonus: slicing works too!
print(collection[0:2]) # ['Python Basics', 'Python Advanced']
# Bonus: iteration works!
for book in collection:
print(f" - {book}")__setitem__: Setting Values by Index
class BookCollection:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def __getitem__(self, index):
return self.books[index]
def __setitem__(self, index, value):
"""Allow setting values: collection[0] = 'New Book'"""
self.books[index] = value
collection = BookCollection()
collection.add_book("Python Basics")
collection.add_book("Python Advanced")
print(collection[0]) # Python Basics
collection[0] = "Python Fundamentals"
print(collection[0]) # Python Fundamentals__contains__: Membership Testing (in)
class BookCollection:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def __contains__(self, item):
"""Allow 'in' operator: if 'Python' in collection:"""
return item in self.books
collection = BookCollection()
collection.add_book("Python Basics")
collection.add_book("Python Advanced")
print("Python Basics" in collection) # True
print("JavaScript Basics" in collection) # FalseA Practical Example: Shopping Cart
Let’s build a shopping cart that uses multiple special methods:
from functools import total_ordering
@total_ordering
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
def __str__(self):
return f"'{self.title}' by {self.author} - ${self.price:.2f}"
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.price})"
def __eq__(self, other):
if not isinstance(other, Book):
return False
return self.title == other.title and self.author == other.author
def __lt__(self, other):
return self.price < other.price
class ShoppingCart:
def __init__(self):
self.items = []
def __len__(self):
"""Number of items in cart"""
return len(self.items)
def __getitem__(self, index):
"""Get item by index"""
return self.items[index]
def __contains__(self, book):
"""Check if book is in cart"""
return book in self.items
def __str__(self):
if not self.items:
return "Empty shopping cart"
cart_str = "Shopping Cart:\n"
for i, book in enumerate(self.items, 1):
cart_str += f" {i}. {book}\n"
cart_str += f"Total: ${self.get_total():.2f}"
return cart_str
def add(self, book):
self.items.append(book)
def remove(self, book):
if book in self.items:
self.items.remove(book)
def get_total(self):
return sum(book.price for book in self.items)
def get_cheapest(self):
if not self.items:
return None
return min(self.items)
def get_most_expensive(self):
if not self.items:
return None
return max(self.items)
# Create books
book1 = Book("Python Crash Course", "Eric Matthes", 39.99)
book2 = Book("Automate the Boring Stuff", "Al Sweigart", 29.99)
book3 = Book("Learning Python", "Mark Lutz", 54.99)
# Create and use cart
cart = ShoppingCart()
cart.add(book1)
cart.add(book2)
cart.add(book3)
# String representation
print(cart)
print()
# Length
print(f"Items in cart: {len(cart)}")
# Indexing
print(f"First item: {cart[0]}")
# Membership
print(f"Is book1 in cart? {book1 in cart}")
# Finding cheapest and most expensive
print(f"\nCheapest: {cart.get_cheapest()}")
print(f"Most expensive: {cart.get_most_expensive()}")Output:
Shopping Cart:
1. 'Python Crash Course' by Eric Matthes - $39.99
2. 'Automate the Boring Stuff' by Al Sweigart - $29.99
3. 'Learning Python' by Mark Lutz - $54.99
Total: $124.97
Items in cart: 3
First item: 'Python Crash Course' by Eric Matthes - $39.99
Is book1 in cart? True
Cheapest: 'Automate the Boring Stuff' by Al Sweigart - $29.99
Most expensive: 'Learning Python' by Mark Lutz - $54.99Arithmetic Methods
You can define how objects work with arithmetic operators:
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
"""Add two Money objects or Money + number"""
if isinstance(other, Money):
return Money(self.amount + other.amount)
return Money(self.amount + other)
def __sub__(self, other):
"""Subtract Money or number"""
if isinstance(other, Money):
return Money(self.amount - other.amount)
return Money(self.amount - other)
def __mul__(self, multiplier):
"""Multiply by a number"""
return Money(self.amount * multiplier)
def __str__(self):
return f"${self.amount:.2f}"
def __repr__(self):
return f"Money({self.amount})"
price1 = Money(29.99)
price2 = Money(39.99)
total = price1 + price2
print(f"Total: {total}") # $69.98
discounted = price1 * 0.8 # 20% off
print(f"20% off: {discounted}") # $23.99
difference = price2 - price1
print(f"Difference: {difference}") # $10.00Other Useful Special Methods
__call__: Make Objects Callable
class BookPrinter:
def __init__(self, prefix=""):
self.prefix = prefix
def __call__(self, book):
"""Make the object callable like a function"""
print(f"{self.prefix}{book}")
printer = BookPrinter(">>> ")
printer("Python Basics") # >>> Python Basics
printer("Python Advanced") # >>> Python Advanced__bool__: Truthiness
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __bool__(self):
"""Cart is True if it has items, False if empty"""
return len(self.items) > 0
cart = ShoppingCart()
if cart:
print("Cart has items")
else:
print("Cart is empty") # This prints
cart.add("Python Basics")
if cart:
print("Cart has items") # This prints__hash__: Making Objects Hashable
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __eq__(self, other):
if not isinstance(other, Book):
return False
return self.title == other.title and self.author == other.author
def __hash__(self):
"""Make Book hashable (can be used in sets and as dict keys)"""
return hash((self.title, self.author))
def __repr__(self):
return f"Book('{self.title}', '{self.author}')"
# Now we can use books in sets!
book1 = Book("Python Basics", "John Doe")
book2 = Book("Python Basics", "John Doe")
book3 = Book("Python Advanced", "Jane Smith")
books = {book1, book2, book3} # Set removes duplicates
print(books) # Only 2 books (book1 and book2 are equal)
# And as dictionary keys!
inventory = {
book1: 10,
book3: 5
}
print(inventory[book1]) # 10Summary
Special methods let you make your classes work seamlessly with Python’s built-in operations:
__str__and__repr__: Control string representation__eq__,__lt__, etc.: Enable comparisons and sorting__len__,__getitem__,__setitem__: Make objects work like containers__contains__: Enableinoperator__add__,__sub__, etc.: Define arithmetic operations__call__: Make objects callable__bool__: Control truthiness__hash__: Make objects hashable for sets and dict keys
By implementing these methods, your custom classes feel like natural parts of Python. They work with built-in functions (len, sorted, min, max), operators (+, -, <, ==, in), and language constructs (if, for).
In the next lesson, we’ll dive into advanced function concepts including closures, variable-length arguments, and type hints.