🧠 Learn how to customize the behavior of your classes using special methods (also known as dunder methods, because they start and end with double underscores __).
These methods allow you to define what happens when built-in operations like:
- Printing an object (
print(obj)) - Comparing objects (
obj1 == obj2) - Hashing for use in sets/dicts
- Truth testing (
if obj:) - Object destruction
are used on instances of your class.
- 🖨️
__str__()– Return a user-friendly string representation - 🧾
__repr__()– Return an unambiguous, developer-friendly representation - ✅
__eq__()– Define equality between two objects - 🔢
__hash__()– Make objects hashable for use in sets or as dict keys - 🚦
__bool__()– Control truthiness of an object - 🗑️
__del__()– Handle cleanup before object destruction - 💡 Hidden notes and best practices for writing clean, idiomatic dunder methods
Used by print() and str() to get a user-friendly string representation.
🔹 Example – Customizing output
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
p = Person("Alice", 30)
print(p) # Output: Alice, 30 years old🔸 If __str__ is not defined, Python falls back to __repr__.
Used by repr() and in the REPL to show unambiguous representations. Useful for debugging.
🔹 Example – Debugging output
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product(name='{self.name}', price={self.price})"🔸 Best Practice: The output should ideally be valid Python code that can recreate the object.
By default, custom objects are compared by identity (memory address). Use __eq__() to compare by value instead.
🔹 Example – Compare by values
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return False
return self.x == other.x and self.y == other.y
p1 = Point(2, 3)
p2 = Point(2, 3)
print(p1 == p2) # True🔸 This lets you check if two objects have the same data, not just if they're the same instance.
If you want to store instances of a class in a set or as dictionary keys, you must define __hash__().
🔹 Example – Make Point hashable
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
p1 = Point(2, 3)
p2 = Point(2, 3)
s = {p1, p2}
print(s) # Output: {Point(x=2, y=3)}🔸 Ensure that __hash__() returns the same value for objects considered equal.
Controls whether an object evaluates to True or False in boolean contexts like if, while, etc.
🔹 Example – Customize object truth value
class Order:
def __init__(self, items):
self.items = items
def __bool__(self):
return len(self.items) > 0
order = Order([])
if order:
print("Order has items")
else:
print("Order is empty") # This will print🔸 By default, any custom object returns True. Override this to make logic more intuitive.
Called when an object is about to be destroyed by the garbage collector.
🔹 Example – Logging deletion
class TempFile:
def __init__(self, name):
self.name = name
print(f"{self.name} created")
def __del__(self):
print(f"{self.name} deleted")
f = TempFile("temp.txt")
del f🔸 Note: Don’t rely on __del__ for critical resource management (like file closing). Use context managers (with) or explicit close methods instead.
Let’s apply multiple dunder methods to a realistic class:
class User:
def __init__(self, username, email):
self.username = username
self.email = email
def __str__(self):
return f"User({self.username}, {self.email})"
def __repr__(self):
return f"User(username='{self.username}', email='{self.email}')"
def __eq__(self, other):
return self.username == other.username and self.email == other.email
def __hash__(self):
return hash((self.username, self.email))
def __bool__(self):
return bool(self.username)
# Usage
u1 = User("alice", "alice@example.com")
u2 = User("alice", "alice@example.com")
print(u1) # User(alice, alice@example.com)
print(repr(u1)) # User(username='alice', email='alice@example.com')
print(u1 == u2) # True
print(hash(u1)) # Same hash as u2
users = {u1, u2}
print(users) # {User(username='alice', email='alice@example.com')}💡 Hidden Tips & Notes
- 🧩 Always implement both
__eq__and__hash__together — if you override one, you should override the other. - 📝
__repr__should ideally return something that could recreate the object (e.g.,eval(repr(obj))works). - 🧱 Avoid side effects in
__del__— it's not guaranteed to run immediately. - 🧵 Use
__bool__to control logic likeif user:rather than checking attributes manually. - 🧠 Prefer immutable types for hashable objects — mutable objects should not be hashable.
- 🧹 Consider using
@dataclass(eq=True)to auto-generate__eq__,__repr__, and more.
| Method | Purpose |
|---|---|
__str__() |
Readable string representation (print(), str()) |
__repr__() |
Unambiguous, developer-friendly representation (repr()) |
__eq__() |
Define object equality |
__hash__() |
Allow use in sets and as dict keys |
__bool__() |
Control truth value |
__del__() |
Called before object destruction |
🎉 Congratulations! You now understand how to customize the behavior of your Python classes using special (dunder) methods like __str__, __repr__, __eq__, and more.
Next up: 🔐 Section 18: Property Management – learn how to control attribute access and validation using @property and setters.