Dunder methods in Python are special functions enclosed in double underscores (e.g., __init__, __str__) that define how objects behave in built-in operations like arithmetic, comparisons, and string conversion. They're called "magic methods" because they automatically trigger during specific actions without being explicitly called.
These methods are part of Python's object model. When you write obj + other, Python checks whether obj.__add__(other) exists. If it does, it calls that method. This allows custom classes to integrate smoothly with Python's syntax and built-in functions.
Without dunder methods, custom objects wouldn't support basic operations like len(obj), str(obj), or obj[key]. They're essential for making user-defined types feel native to the language.
Python uses dunder methods to map operator syntax and built-in function calls to underlying method calls. For example, when you call len(my_list), Python internally runs my_list.__len__().
This mapping happens at runtime. If a dunder method isn't defined, Python raises a TypeError. For instance, if you try to add two instances of a class that doesn't define __add__, Python throws:
>>> class Point:
... def __init__(self, x, y):
... self.x, self.y = x, y
...
>>> p1 = Point(1, 2)
>>> p2 = Point(3, 4)
>>> p1 + p2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Point' and 'Point'
To fix this, define __add__:
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
Now p1 + p2 returns a new Point object with coordinates (4, 6).
There are dozens of dunder methods. Here are the most frequently used ones, grouped by purpose.
| Dunder Method | Purpose | Example Use Case |
|---|---|---|
__init__ |
Initialize object attributes after creation | Set default values for a BankAccount upon instantiation |
__str__ |
Define human-readable string representation | Return "User(name='Alice', email='[email protected]')" |
__repr__ |
Define unambiguous debugging representation | Return "User('Alice', '[email protected]')"' for logging |
__len__ |
Support len() on custom objects |
Return count of items in a custom list container |
__eq__ |
Define equality comparison (==) |
Compare two Student objects by ID, not memory address |
__getitem__ |
Enable indexing via obj[i] |
Allow a DatabaseRow class to access fields by column name |
Some methods are used less often but are critical for advanced features. For example, __call__ lets an object act like a function:
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter()) # Output: 1
print(counter()) # Output: 2
Dunder methods are not private methods. The double show prefix is a naming convention, not a privacy mechanism. Unlike _private_method, which is protected by naming, __dunder_method is accessible but intended for internal use.
A common misconception is that dunder methods are only for built-in types. They're not. You define them in your own classes to customize behavior. A Vector class can implement __mul__ to handle scalar multiplication.
Another error: assuming __str__ and __repr__ do the same thing. They don't. __str__ is for users. __repr__ is for developers. The former should be readable; the latter should be precise and unambiguous.
For example, a datetime object's __str__ gives "2024-04-05 14:30:00", while __repr__ shows "datetime.datetime(2024, 4, 5, 14, 30)". The latter helps debug code.
Consider a configuration class that loads settings from a file. Without dunder methods, accessing config values would require explicit method calls.
With __getitem__ and __setitem__, you can treat it like a dictionary:
class Config:
def __init__(self, path):
self._data = self._load(path)
def _load(self, path):
# Simulate loading JSON or YAML
return {"debug": True, "port": 8080}
def __getitem__(self, key):
return self._data.get(key)
def __setitem__(self, key, value):
self._data[key] = value
config = Config("settings.json")
print(config["debug"]) # True
config["debug"] = False
This pattern makes the class feel like a native dict. Users don't need to learn extra methods. The interface is consistent with Python's core data structures.
I built a similar system for a web app that managed API keys. Using __getattr__ and __setattr__, I enabled dot notation access like config.api_key alongside bracket access. It reduced errors and improved readability.
Not all dunder methods are meant to be overridden. Some are reserved for internal use. For example, __new__ controls object creation and is rarely needed. Most classes don't override it unless implementing singletons or custom metaclasses.
Another subtle point: __eq__ must be reflexive, symmetric, and transitive for correct behavior. If a == b is True, then b == a must be True. If a == b and b == c, then a == c must hold. Violating this causes bugs in sets and dictionaries.
Also, defining __eq__ without __hash__ breaks hashing. If two objects compare equal, they must have the same hash. Otherwise, they won't work in sets or as dictionary keys.
For example:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# This fails:
p1 = Point(1, 2)
p2 = Point(1, 2)
print({p1, p2}) # Raises TypeError: unhashable type: 'Point'
To fix it, define __hash__:
def __hash__(self):
return hash((self.x, self.y))
Now the set contains only one unique point.
Understanding dunder methods is important for writing idiomatic Python. The official documentation details each method's behavior and usage. Refer to the Python Data Model documentation for authoritative specifications.
For deeper insight into how Python resolves method calls, see the Call Expression section in the language reference.
Dunder methods aren't optional extras. They're the foundation of Python's flexibility. Mastering them turns basic classes into useful, expressive tools.