Special Methods in Python

Learn the most important special methods in Python: __str__, __repr__, __eq__, __hash__, __bool__, __call__, __getitem__, and more, with practical examples.

10 min read

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.

pythonpython
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.

texttext
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.

pythonpython
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.

texttext
True
False
False

When 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.

pythonpython
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.

texttext
red

The 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.

pythonpython
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:.

texttext
False
True

The 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.

pythonpython
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.

texttext
3
Maya
Leo
Maya
Kai
Leo
True

The 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.

pythonpython
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.

texttext
True
False

The 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.

pythonpython
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.

texttext
15
None
True

The 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.

pythonpython
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.

texttext
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.

pythonpython
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.

texttext
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__.

pythonpython
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:

pythonpython
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.

texttext
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.

pythonpython
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.

texttext
Sum: 499999500000
Elapsed: 0.0156s

The 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.

pythonpython
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.

texttext
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between `__str__` and `__repr__`?

`__str__` is meant to return a human-readable description of the object, used by str() and print(). `__repr__` is meant to return an unambiguous representation useful for debugging, used by repr() and displayed in the interactive interpreter. If you only implement one, implement `__repr__`, because Python falls back to `__repr__` when `__str__` is not defined.

When should I implement `__eq__` without `__hash__`?

Implement `__eq__` without `__hash__` when your objects represent mutable things that should not be used as dictionary keys or set elements. Python automatically sets `__hash__` to None when you define `__eq__` without defining `__hash__`, which makes the object unhashable. This is the safe default for mutable objects. For immutable value objects, implement both `__eq__` and `__hash__` together.

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.