Special methods (also called "magic methods" or "dunder methods") let you customize how objects behave with built-in Python operations. They're the methods with double underscores like __init__, __str__, and __add__. Understanding them makes your classes more intuitive and Pythonic.
In this lesson, you'll learn about string representation methods (__str__ and __repr__), operator overloading for mathematical operations, comparison operators, and context managers. These features make your objects work naturally with Python's built-in functions and operators.
What You'll Learn
- Understanding dunder methods (str, repr)
- Operator overloading (add, eq, etc.)
- Context managers (enter, exit)
- Making objects behave like built-in types
- Best practices for special methods
String Representation: str and repr
These methods control how objects are converted to strings:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""User-friendly string representation."""
return f"Point({self.x}, {self.y})"
def __repr__(self):
"""Developer-friendly string representation."""
return f"Point({self.x}, {self.y})"
point = Point(3, 4)
print(str(point)) # Point(3, 4) - uses __str__
print(repr(point)) # Point(3, 4) - uses __repr__
print(point) # Point(3, 4) - print() uses __str__
__str__ is for end users, __repr__ should be unambiguous and ideally recreate the object:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
person = Person("Alice", 25)
print(str(person)) # Alice, 25 years old
print(repr(person)) # Person('Alice', 25)
Operator Overloading
You can define how operators work with your objects:
Arithmetic Operators
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Define + operator."""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Define - operator."""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Define * operator for scalar multiplication."""
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
Comparison Operators
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __eq__(self, other):
"""Define == operator."""
return self.pages == other.pages
def __lt__(self, other):
"""Define < operator."""
return self.pages < other.pages
def __le__(self, other):
"""Define <= operator."""
return self.pages <= other.pages
def __str__(self):
return f"{self.title} ({self.pages} pages)"
book1 = Book("Python Basics", 300)
book2 = Book("Advanced Python", 500)
book3 = Book("Quick Guide", 300)
print(book1 == book3) # True (same pages)
print(book1 < book2) # True (300 < 500)
print(book2 <= book1) # False
Other Useful Operators
class Container:
def __init__(self, items):
self.items = items
def __len__(self):
"""Define len() function."""
return len(self.items)
def __getitem__(self, index):
"""Define indexing [] operator."""
return self.items[index]
def __setitem__(self, index, value):
"""Define item assignment."""
self.items[index] = value
def __contains__(self, item):
"""Define 'in' operator."""
return item in self.items
def __str__(self):
return str(self.items)
container = Container([1, 2, 3, 4, 5])
print(len(container)) # 5
print(container[2]) # 3
print(3 in container) # True
container[0] = 10
print(container) # [10, 2, 3, 4, 5]
Context Managers: enter and exit
Context managers let you use objects with the with statement for resource management:
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Called when entering 'with' block."""
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block."""
if self.file:
self.file.close()
return False # Don't suppress exceptions
# Using the context manager
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")
# File is automatically closed
# Custom context manager for timing
import time
class Timer:
def __init__(self, description):
self.description = description
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
print(f"{self.description} took {elapsed:.4f} seconds")
return False
with Timer("Processing data"):
time.sleep(1)
# Processing data took 1.0012 seconds
Practical Examples
Here are comprehensive examples combining multiple special methods:
# Example 1: Fraction class with operator overloading
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
self.numerator = numerator
self.denominator = denominator
self._simplify()
def _simplify(self):
"""Simplify the fraction."""
from math import gcd
common = gcd(self.numerator, self.denominator)
self.numerator //= common
self.denominator //= common
def __add__(self, other):
"""Add two fractions."""
new_num = self.numerator * other.denominator + other.numerator * self.denominator
new_den = self.denominator * other.denominator
return Fraction(new_num, new_den)
def __mul__(self, other):
"""Multiply two fractions."""
return Fraction(self.numerator * other.numerator,
self.denominator * other.denominator)
def __eq__(self, other):
"""Check if fractions are equal."""
return (self.numerator == other.numerator and
self.denominator == other.denominator)
def __str__(self):
return f"{self.numerator}/{self.denominator}"
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
f1 = Fraction(1, 2)
f2 = Fraction(1, 4)
print(f1 + f2) # 3/4
print(f1 * f2) # 1/8
print(f1 == f2) # False
# Example 2: Custom list with special methods
class NumberList:
def __init__(self, numbers):
self.numbers = list(numbers)
def __len__(self):
return len(self.numbers)
def __getitem__(self, index):
return self.numbers[index]
def __setitem__(self, index, value):
self.numbers[index] = value
def __add__(self, other):
"""Concatenate two NumberLists."""
return NumberList(self.numbers + other.numbers)
def __contains__(self, item):
return item in self.numbers
def __str__(self):
return str(self.numbers)
def sum(self):
return sum(self.numbers)
def average(self):
return sum(self.numbers) / len(self.numbers) if self.numbers else 0
list1 = NumberList([1, 2, 3])
list2 = NumberList([4, 5, 6])
combined = list1 + list2
print(combined) # [1, 2, 3, 4, 5, 6]
print(len(combined)) # 6
print(3 in combined) # True
print(combined.sum()) # 21
Try It Yourself
Practice implementing special methods:
-
Complex Number Class: Create a
Complexclass with__add__,__sub__,__mul__, and__str__methods. -
Custom Dictionary: Create a class that behaves like a dictionary using
__getitem__,__setitem__, and__contains__. -
Timer Context Manager: Create a context manager that times code execution and handles exceptions gracefully.
-
Matrix Class: Create a
Matrixclass with__add__,__mul__, and__str__methods for matrix operations. -
Shopping Cart: Create a cart class with
__len__,__getitem__,__contains__, and__add__for combining carts.
Summary
Special methods let you customize how your objects interact with Python's built-in operations. __str__ and __repr__ control string representation. Operator overloading methods like __add__ and __eq__ make objects work naturally with operators. Context managers (__enter__ and __exit__) enable resource management with the with statement.
These methods make your classes more intuitive and Pythonic. They're used throughout Python's standard library and are essential for creating professional, user-friendly classes. Understanding them helps you write code that feels natural and integrates well with Python's ecosystem.
What's Next?
In the next lesson, we'll explore modules and importing. You'll learn how to organize code into modules, use different import styles, and understand Python's module search path. This knowledge is essential for building larger, well-organized projects.