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
False

Ordering 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: True

Shortcut: 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)  # False

A 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.99

Arithmetic 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.00

Other 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])  # 10

Summary

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__: Enable in operator
  • __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.