Special methods, also called dunder methods for their double-underscore names, are the hooks that let your custom objects respond to Python syntax. When you implement a special method on your class, the corresponding built-in function or operator automatically delegates to it.
The article on the Python data model explained the protocol system that connects syntax to method calls. This article covers the individual special methods you will implement most often, with practical examples you can adapt to your own classes.
The repr and str methods: string representations
__repr__ returns an unambiguous string for debugging. The goal is that a developer can identify the object and its state from the output.
__str__ returns a human-readable string for display. The print function calls it, and if it is not defined, Python falls back to repr instead.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(3, 4)
print(repr(p))
print(str(p))
print(p)The repr output includes the class name and constructor-style arguments. The str output shows a clean coordinate pair, and print uses that same str output.
Point(3, 4)
(3, 4)
(3, 4)Use the !r format specifier inside f-strings to call repr on attribute values. This avoids ambiguity between Point(3, 4) and Point("3", "4"). Always implement a debug representation at minimum on every class you write.
The eq and hash methods: equality and hashing
__eq__ defines when two objects are equal. Python calls it for the == operator, and without it the default from object falls back to identity comparison, where two objects are equal only when they are the same object.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
alice1 = Person("Alice", 30)
alice2 = Person("Alice", 30)
bob = Person("Bob", 25)
print(alice1 == alice2)
print(alice1 == bob)
print(alice1 == "Alice")Two Person objects with the same name and age are equal. A Person is not equal to a string, and NotImplemented signals Python to try the reflected comparison.
True
False
FalseWhen you define equality without a matching hash method, Python sets the object's hash to None, making instances unhashable. This is the correct default for mutable objects that should not be dictionary keys.
For immutable value objects, implement both together, since the hash must match the equality definition and equal objects must have equal hashes.
class Color:
def __init__(self, r, g, b):
self._r = r
self._g = g
self._b = b
def __eq__(self, other):
if not isinstance(other, Color):
return NotImplemented
return (self._r, self._g, self._b) == (other._r, other._g, other._b)
def __hash__(self):
return hash((self._r, self._g, self._b))
palette = {Color(255, 0, 0): "red", Color(0, 255, 0): "green"}
print(palette[Color(255, 0, 0)])A new Color with the same RGB values finds the entry in the dictionary because the hash and equality match.
redThe bool method: truth value
Python calls __bool__ to decide whether an object is truthy in an if statement. If it is not defined, Python falls back to length, treating a nonzero length as true, and if neither is defined the object is always truthy.
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __bool__(self):
return len(self.items) > 0
cart = ShoppingCart()
print(bool(cart))
cart.add("book")
print(bool(cart))An empty cart returns False, and a cart with at least one item returns True. This lets you write natural conditions like if cart:.
False
TrueThe len and getitem methods: the sequence protocol
__len__ returns the length, and __getitem__ returns the item at a given index. Together they enable indexing, slicing, iteration, and membership testing on your custom objects.
class Team:
def __init__(self, members):
self._members = list(members)
def __len__(self):
return len(self._members)
def __getitem__(self, index):
return self._members[index]
team = Team(["Maya", "Kai", "Leo"])
print(len(team))
print(team[0])
print(team[-1])
for member in team:
print(member)
print("Kai" in team)The len function calls the length method, and indexing and slicing call the getitem method. The for loop works because Python calls getitem with increasing integers until IndexError.
3
Maya
Leo
Maya
Kai
Leo
TrueThe in operator above works through fallback iteration, since this class has no dedicated membership method. Adding methods for item assignment and deletion would let the same class support statements like team[0] = "Sam".
The contains method: membership testing
When you define a membership method, Python calls it directly for the in operator instead of falling back to iteration. This is more efficient and gives you full control over what membership means.
class TemperatureRange:
def __init__(self, low, high):
self.low = low
self.high = high
def __contains__(self, value):
return self.low <= value <= self.high
comfortable = TemperatureRange(18, 24)
print(20 in comfortable)
print(10 in comfortable)A value is considered "in" the range when it falls between the low and high bounds inclusive. This approach reads naturally and is far more efficient than iterating through every possible value in a continuous range.
True
FalseThe call method: callable instances
__call__ lets you call an instance like a function. This is useful for objects that maintain state between invocations, like parameterized filters or processors.
class ThresholdFilter:
def __init__(self, minimum):
self.minimum = minimum
def __call__(self, value):
return value if value >= self.minimum else None
above_ten = ThresholdFilter(10)
print(above_ten(15))
print(above_ten(5))
print(callable(above_ten))Each instance remembers its threshold value and applies it when called like a function. The built-in callable function confirms the object supports this invocation protocol.
15
None
TrueThe add and iadd methods: numeric operators
__add__ defines the + operator and should create and return a new object, while a separate in-place method defines the += operator for modifying the object directly.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)The + operator calls that method, which creates and returns a brand new Vector. The original v1 object remains unchanged by the addition.
Vector(4, 6)The in-place version handles the += operator differently: it modifies the existing object and must return self instead of a new instance.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __iadd__(self, other):
if not isinstance(other, Vector):
return NotImplemented
self.x += other.x
self.y += other.y
return self
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 += v2
print(v1)Returning self is required because Python binds the result of += back to the left-hand name. The += operator here modifies v1 in place rather than creating a new object.
Vector(4, 6)Ordering methods and total_ordering
Comparison operators each map to their own method: less-than, less-or-equal, greater-than, and greater-or-equal. The @functools.total_ordering decorator fills in the three you skip once you provide equality and __lt__.
from functools import total_ordering
@total_ordering
class Score:
def __init__(self, points):
self.points = points
def __eq__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.points == other.points
def __lt__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.points < other.points
def __repr__(self):
return f"Score({self.points})"With just equality and less-than defined, the decorator automatically generates the remaining operators for you without any extra code. Now you can test the full set of comparisons on Score instances:
a = Score(100)
b = Score(200)
print(a < b)
print(a <= b)
print(sorted([b, a]))The decorator generates the remaining three comparisons automatically from the two you wrote. The sorted function works without a key because the comparison methods are in place.
True
True
[Score(100), Score(200)]The enter and exit methods: context managers
__enter__ and __exit__ let an object work with the with statement. The enter method runs at the start of the block, and the exit method runs at the end, whether the block completes normally or raises an exception.
class Timer:
def __enter__(self):
import time
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False
with Timer():
total = sum(range(1_000_000))
print(f"Sum: {total}")The timer captures a start time on entry, computes elapsed time on exit, and prints the result regardless of whether the block succeeded or failed.
Sum: 499999500000
Elapsed: 0.0156sThe exit method receives three arguments: exception type, value, and traceback, all of which are None when no exception occurred. Returning False lets exceptions propagate, while returning True would suppress them instead.
The init and new methods: object creation
__new__ creates a new instance and runs before __init__, which then initializes the already-created instance. You rarely need to override the first one unless you are subclassing immutable types or implementing singletons.
class PositiveInt(int):
def __new__(cls, value):
return super().__new__(cls, abs(value))
num = PositiveInt(-5)
print(num)
print(type(num))The built-in int type is immutable, so the init method cannot change the stored value after the instance already exists. Overriding the new method lets you build the corrected value before creation, which is why this pattern only shows up when subclassing immutable types.
5
<class '__main__.PositiveInt'>For most classes, init is the only one of the two you need. Reserve new for cases where you must control instance creation itself.
How to choose which methods to implement
Start with a debug representation. Every class benefits from a useful repr, and from there you can consider what your class represents.
For value objects, add equality when comparison matters, add hashing when the object is immutable and should work as a dictionary key, and add a display string when the debug output is too technical to show users.
For collection-like objects, implement length and indexing together, and add a dedicated membership method when checking "in" can be more efficient than iterating.
For callable objects, implement the call method. For resource wrappers, implement the enter and exit pair. For numeric types, implement the arithmetic operators your type should support.
The article on customizing Python objects with magic methods covers more advanced patterns, including attribute access control through the special methods that intercept reading and writing attributes.
Rune AI
Key Insights
__repr__is for debugging;__str__is for display. Implement__repr__first.__eq__defines value equality; Python sets__hash__to None when you define__eq__alone.__bool__controls truthiness; implement it to make your objects work with if statements.__call__makes instances callable like functions, useful for stateful operations.__getitem__and__len__together make an object support iteration, indexing, and membership testing.
Frequently Asked Questions
What is the difference between `__str__` and `__repr__`?
When should I implement `__eq__` without `__hash__`?
Conclusion
Special methods are your toolkit for making custom objects feel like they belong in Python. Start with __repr__ and __str__ so your objects print usefully. Add __eq__ when comparison matters. Implement __len__ and __getitem__ for collection-like objects. Each special method you add makes your class work more naturally with the language syntax your users already know.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.