
Python Dunder Methods are the backbone of Python’s object-oriented programming. They define how objects interact with built-in functions, operators, and Python’s core functionality. Think of them as the translators between your custom objects and Python’s built-in operations.These special methods are everywhere in Python, quietly working behind the scenes to make your code more intuitive and powerful. Whether you’re calling len()
on a list or adding two numbers with +
, dunder methods are making it happen.
Dunder methods are special methods in Python that start and end with double underscores, used to define how objects behave with operators and built-in functions.
The term “dunder” comes from “double underscore” – these methods start and end with two underscores, like __init__
or __str__
. Guido van Rossum, Python’s creator, introduced this convention in the early days of Python to distinguish special methods from regular ones. This naming pattern signals to both Python and other developers that these methods have special significance.
Understanding dunder methods transforms you from someone who uses Python to someone who truly understands how Python works. They’re your ticket to creating objects that feel native to the language, seamlessly integrating with Python’s built-in functions and operators.
💡: Explore all advanced python topics.
Table of contents
- Understanding Python Dunder Methods
- Object Creation and Destruction
- Representation and Formatting
- Python Dunder Methods For Comparison and Equality
- Arithmetic Operations
- Attribute Access
- Container Methods
- Other Important Python Dunder Methods
- Best Practices and Common Mistakes
- Conclusion
- Python Dunder Method FAQs
Understanding Python Dunder Methods
Dunder methods integrate seamlessly with Python’s object model, acting as hooks that Python calls automatically when certain operations occur. When you write len(my_list)
, Python actually calls my_list.__len__()
behind the scenes. This elegant design makes Python feel natural and intuitive.
The power of dunder methods lies in operator overloading and built-in function support. They allow your custom objects to work with Python’s operators (+
, -
, ==
, etc.) and built-in functions (len()
, str()
, repr()
, etc.) just like built-in types do.
Let’s examine how built-in types use dunder methods. When you call len("Hello")
, Python invokes "Hello".__len__()
. Similarly, "Hello" + "World"
triggers "Hello".__add__("World")
. Every built-in type in Python implements relevant dunder methods to support these operations.
Here’s a simple example showing dunder methods in action:
# Behind the scenes, Python translates these operations
my_string = "Python"
print(len(my_string)) # Calls my_string.__len__()
print(str(my_string)) # Calls my_string.__str__()
print(my_string + " rocks") # Calls my_string.__add__(" rocks")
Code language: PHP (php)
The categories of dunder methods roughly align with different aspects of object behavior: creation and destruction, representation, comparison, arithmetic operations, attribute access, and container emulation. Each category serves specific purposes in making your objects behave like first-class Python citizens.

Pro Tip: Don’t call dunder methods directly in your code. Use the built-in functions and operators instead. Write len(obj)
rather than obj.__len__()
– it’s more readable and allows Python to optimize the call.
Object Creation and Destruction
The lifecycle of every Python object begins with creation and eventually ends with destruction. Two fundamental dunder methods control this process: __init__
for initialization and __del__
for cleanup.
init: The Constructor
__init__
is probably the most familiar dunder method. It initializes new objects, setting up their initial state. Think of it as your object’s birth certificate – it defines what the object looks like when it first comes into existence.
class BankAccount:
def __init__(self, account_holder, initial_balance=0):
self.account_holder = account_holder
self.balance = initial_balance
print(f"Account created for {account_holder}")
def __del__(self):
print(f"Account for {self.account_holder} is being closed")
# Creating instances
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob")
# When objects go out of scope or program ends, __del__ is called
Code language: Python (python)
new: The True Constructor
While __init__
initializes objects, __new__
actually creates them. You rarely need to override __new__
, but it’s useful for controlling object creation in singletons or immutable types.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, 'initialized'):
self.initialized = True
print("Singleton initialized")
del: The Destructor
__del__
handles cleanup when objects are destroyed. Use it sparingly – Python’s garbage collector usually handles memory management automatically. It’s most useful for closing files, network connections, or other resources.
The timing of __del__
calls is unpredictable, so don’t rely on it for critical cleanup. Use context managers (with
statements) for reliable resource management instead.
Representation and Formatting
How your objects present themselves to the world matters. Three dunder methods control string representation: __str__
for human-readable output, __repr__
for developer-friendly debugging, and __format__
for custom formatting.
str vs repr: The Dynamic Duo
__str__
creates readable representations for end users, while __repr__
provides unambiguous representations for developers. A good rule of thumb: __str__
should be pretty, __repr__
should be precise.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point at ({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __format__(self, format_spec):
if format_spec == 'coords':
return f"{self.x},{self.y}"
return str(self)
# Usage examples
p = Point(3, 4)
print(str(p)) # Point at (3, 4)
print(repr(p)) # Point(3, 4)
print(f"{p:coords}") # 3,4
Code language: Python (python)
The __repr__
method should ideally return a string that, when passed to eval()
, recreates the object. If that’s not possible, return something enclosed in angle brackets like <Point object at 0x...>
.
Pro Tip 💡: If you only implement one representation method, choose __repr__
. Python will use it for __str__
if __str__
isn’t defined, but not vice versa.
format: Custom Formatting Magic
__format__
enables your objects to work with f-strings and the format()
function. It’s incredibly powerful for creating domain-specific formatting options.
class Currency:
def __init__(self, amount, code="USD"):
self.amount = amount
self.code = code
def __format__(self, format_spec):
if format_spec == 'short':
return f"{self.code}{self.amount:.2f}"
elif format_spec == 'long':
return f"{self.amount:.2f} {self.code}"
return f"{self.amount:.2f}"
price = Currency(29.99)
print(f"{price:short}") # USD29.99
print(f"{price:long}") # 29.99 USD
Code language: Python (python)
Python Dunder Methods For Comparison and Equality
Comparing objects is fundamental to programming. Python provides six comparison dunder methods that make your objects work seamlessly with comparison operators.
Dunder Method | Operator | Description |
__eq__ | == | Equal to |
__ne__ | != | Not equal to |
__lt__ | < | Less than |
__le__ | <= | Less than or equal |
__gt__ | > | Greater than |
__ge__ | >= | Greater than or equal |
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.grade == other.grade
def __lt__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.grade < other.grade
def __le__(self, other):
return self < other or self == other
def __gt__(self, other):
return not self <= other
def __ge__(self, other):
return not self < other
def __ne__(self, other):
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result
# Usage
alice = Student("Alice", 85)
bob = Student("Bob", 92)
print(alice < bob) # True
print(alice == bob) # False
print(alice >= bob) # False
Code language: Python (python)
Pro Tip 💡: Always return NotImplemented
(not NotImplementedError
) when comparing with incompatible types. This allows Python to try the reverse operation or raise an appropriate error.
Implementing comparison methods enables sorting and other comparison-based operations on your objects. Python’s functools.total_ordering
decorator can generate missing comparison methods from __eq__
and at least one other ordering method.
Arithmetic Operations
Arithmetic dunder methods transform your objects into mathematical entities. They enable natural mathematical notation with your custom classes, making code more intuitive and readable.
The main arithmetic dunder methods include:
__add__
(+),__sub__
(-),__mul__
(*),__truediv__
(/)__floordiv__
(//),__mod__
(%),__pow__
(**)__and__
(&),__or__
(|),__xor__
(^)
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar):
return self.__mul__(scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Mathematical operations feel natural
v1 = Vector(2, 3)
v2 = Vector(1, 4)
v3 = v1 + v2 # Vector(3, 7)
v4 = v1 * 2 # Vector(4, 6)
v5 = 3 * v1 # Vector(6, 9) - uses __rmul__
Code language: Python (python)
The __rmul__
method handles reverse multiplication (when your object is on the right side of the operator). Python tries the regular method first, then falls back to the reverse method if needed.
Question for reflection: How might you implement division for vectors? Would you support element-wise division, or perhaps division by a scalar only?
Attribute Access
Controlling how attributes are accessed, set, and deleted gives you powerful ways to create dynamic, flexible objects. Four key dunder methods manage attribute access in Python.

Dynamic Attribute Handling
__getattr__
is called when Python can’t find an attribute through normal lookup. It’s perfect for creating proxy objects or implementing attribute forwarding.
class DynamicObject:
def __init__(self):
self._data = {}
def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name.startswith('_'):
# Allow private attributes to be set normally
super().__setattr__(name, value)
else:
# Store public attributes in our data dictionary
if not hasattr(self, '_data'):
super().__setattr__('_data', {})
self._data[name] = value
def __delattr__(self, name):
if name in self._data:
del self._data[name]
else:
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
# Usage
obj = DynamicObject()
obj.name = "Python" # Stored in _data
obj.version = 3.9 # Stored in _data
print(obj.name) # Retrieved from _data
del obj.version # Removed from _data
Code language: Python (python)
The Attribute Access Flow
Understanding the order of attribute access helps you implement these methods correctly:
__getattribute__
is called for every attribute access- If
__getattribute__
raisesAttributeError
,__getattr__
is called - Setting attributes calls
__setattr__
- Deleting attributes calls
__delattr__
Pro Tip: Be extremely careful with __getattribute__
– it’s called for every attribute access and can easily cause infinite recursion. Use super().__getattribute__()
to access attributes safely within it.
Important ⚠️: Be mindful about mixing up the attribute access behaviour with python descriptors, which are for class-level attribute management, where these are at the instance-level.
Container Methods
Container dunder methods make your objects behave like built-in containers (lists, dictionaries, sets). They enable natural syntax for accessing, modifying, and iterating over your custom collections.
Essential container methods include:
__len__
: Returns container length forlen()
function__getitem__
,__setitem__
,__delitem__
: Enable indexing with[]
__iter__
: Makes objects iterable in for loops__contains__
: Enablesin
operator
class SimpleList:
def __init__(self):
self._items = []
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
def __iter__(self):
return iter(self._items)
def __contains__(self, item):
return item in self._items
def append(self, item):
self._items.append(item)
def __str__(self):
return f"SimpleList({self._items})"
# Behaves like a built-in list
my_list = SimpleList()
my_list.append("Python")
my_list.append("JavaScript")
print(len(my_list)) # 2
print(my_list[0]) # Python
print("Python" in my_list) # True
for item in my_list: # Iteration works
print(item)
Code language: Python (python)
This implementation creates a list-like container that supports all the operations you’d expect. The __iter__
method makes it work with for loops, list comprehensions, and other iteration contexts.
Other Important Python Dunder Methods
Several other dunder methods provide specialized functionality that can make your objects incredibly powerful and flexible.
call: Making Objects Callable
__call__
transforms objects into function-like entities. When you add parentheses after an object, Python calls its __call__
method.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
# Create callable objects
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10 (calls double.__call__(5))
print(triple(4)) # 12 (calls triple.__call__(4))
# Use in higher-order functions
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers)) # [2, 4, 6, 8, 10]
Code language: Python (python)
Context Management with enter and exit
Context managers enable the with
statement, providing automatic resource management and cleanup.
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"Opening {self.filename}")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing {self.filename}")
if self.file:
self.file.close()
<em># Return False to propagate exceptions</em>
return False
# Usage with 'with' statement
# with FileManager("data.txt", "w") as f:
# f.write("Hello, World!")
# File automatically closed when leaving the with block
Code language: Python (python)
Tip 💡: Learn about python context managers more in depth.
hash: Enabling Hashable Objects
__hash__
makes objects usable as dictionary keys and set members. Objects that compare equal must have the same hash value.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def __str__(self):
return f"Point({self.x}, {self.y})"
# Now points can be dictionary keys or set members
points = {Point(1, 2): "First", Point(3, 4): "Second"}
unique_points = {Point(1, 2), Point(1, 2), Point(3, 4)} # Set with 2 unique points
Code language: Python (python)
Best Practices and Common Mistakes
Implementing dunder methods effectively requires understanding when and how to use them properly. Here are key guidelines I’ve learned through experience and seeing common pitfalls.
When to Implement Dunder Methods
Implement dunder methods when they make your objects more intuitive and Pythonic. If your class represents something that naturally supports certain operations, add the corresponding dunder methods. A Vector
class should support addition, a BankAccount
might support comparison based on balance, and a custom collection should support len()
and iteration.
Don’t implement dunder methods just because you can. Each method should serve a clear purpose and feel natural to users of your class. Overloading operators in unexpected ways confuses rather than helps.
The eq and hash Consistency Rule
This is the most common mistake I see: implementing __eq__
without considering __hash__
. If two objects compare equal, they must have the same hash value. If you override __eq__
, you usually need to override __hash__
too, or set __hash__ = None
to make objects unhashable.
# Problematic - can break when used in sets or as dict keys
class BadExample:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return isinstance(other, BadExample) and self.value == other.value
# Missing __hash__ override!
# Correct implementation
class GoodExample:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return isinstance(other, BadExample) and self.value == other.value
def __hash__(self):
return hash(self.value)
Code language: Python (python)
Performance Considerations
Dunder methods are called frequently, so their performance matters. Keep implementations simple and fast. Avoid complex calculations in methods like __hash__
or __eq__
that might be called repeatedly.
For container classes, consider caching length calculations if computing length is expensive. Store the result and update it when the container changes rather than recalculating every time __len__
is called.
Error Handling Best Practices
Return NotImplemented
(not NotImplementedError
) when operations aren’t supported between types. This allows Python to try alternative approaches or provide appropriate error messages.
def __add__(self, other):
if isinstance(other, MyClass):
# Perform addition
return MyClass(self.value + other.value)
return NotImplemented # Let Python handle the error
Code language: Python (python)
Always validate inputs in dunder methods. Users will try unexpected operations, and clear error messages save debugging time.
Conclusion
Mastering Python Dunder Methods transforms your understanding of Python from surface-level syntax to deep comprehension of how the language works. These special methods are the bridge between your custom objects and Python’s built-in functionality, enabling you to create classes that feel native to the language.
The journey from basic classes to dunder-method-powered objects is transformative. Your code becomes more intuitive, your objects integrate seamlessly with Python’s ecosystem, and you gain the ability to create truly Pythonic APIs. Start with the basics – __init__
, __str__
, and __repr__
– then gradually add other methods as your needs grow.
Remember that dunder methods should enhance clarity, not obscure it. Every method you implement should make your objects more natural to use. When in doubt, ask yourself: “Would this operation feel obvious to someone using my class?”
Keep experimenting, keep building, and most importantly, keep the Python zen in mind: “There should be one obvious way to do it.” 🐍
For more deeper exploration, check out the official Python documentation on special methods and explore Python’s data model guide.
Python Dunder Method FAQs
__str__
provides a readable string for users, while __repr__
offers a detailed representation for developers, often for debugging. Use __str__
for end-user display and __repr__
for development and logging.
It’s not recommended, as double-underscore names are reserved for Python’s special methods. Create regular methods with descriptive names instead.
No, you can use functools.total_ordering
decorator with just __eq__
and one ordering method (like __lt__
), and Python will generate the rest.
__getattribute__
is called for every attribute access, while __getattr__
is only called when normal attribute lookup fails. Use __getattr__
for dynamic attributes and be very careful with __getattribute__
.
Discover more from CodeSamplez.com
Subscribe to get the latest posts sent to your email.
Leave a Reply