Dunder ("double underscore") methods are special methods Python calls automatically in response to operations like +, print(), len() or ==. You rarely call them directly; you define them so your own classes behave like built-in types.
v1 + v2 # v1.__add__(v2)
print(obj) # obj.__str__()
len(obj) # obj.__len__()
obj == other # obj.__eq__(other)
Every class inherits defaults from object. For built-ins like int and list these are already sensible; for your own classes they are usually not, so you override them.
Object creation: __new__ and __init__#
Creating an object takes two dunder calls:
__new__(cls, ...)creates and returns the empty object. Called first. Override rarely (e.g. Singleton).__init__(self, ...)fillsself.__dict__with instance variables. Called second, always returnsNone. Override almost always.
See Classes and Objects for the full walkthrough.
String representations: __str__ and __repr__#
Without these, printing your object shows something like <Dog object at 0x7f...>.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self): # for end users
return f"{self.name} ({self.age} years)"
def __repr__(self): # for developers: unambiguous, ideally valid code
return f"Dog(name={self.name!r}, age={self.age!r})"
d = Dog("Bruno", 3)
print(d) # Bruno (3 years)
repr(d) # Dog(name='Bruno', age=3)
__str__ |
__repr__ |
|
|---|---|---|
| Purpose | Human-readable | Developer/debug-readable |
| Triggered by | print(obj), str(obj), f-strings |
repr(obj), the REPL, inside containers |
| Fallback | Uses __repr__ if not defined |
Default <... object at 0x...> |
Define __repr__ at minimum: str() falls back to it, and lists of your objects print with it.
Comparisons: __eq__ and __lt__#
By default == compares identity (same object in memory), so two equal-looking objects are not equal.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Dog):
return NotImplemented # let Python try the other side
return (self.name, self.age) == (other.name, other.age)
def __lt__(self, other):
return self.age < other.age # order dogs by age
print(Dog("Bruno", 3) == Dog("Bruno", 3)) # True
dogs = [Dog("Max", 5), Dog("Bruno", 3), Dog("Charlie", 1)]
dogs.sort() # sort() uses __lt__
print([d.name for d in dogs]) # ['Charlie', 'Bruno', 'Max']
selfis the left operand,otherthe right.- Without
__lt__,d1 < d2on custom objects raisesTypeError. - Defining
__eq__sets__hash__toNone, so the objects can no longer go in sets or be dict keys unless you also define__hash__.
Arithmetic: __add__#
Define __add__ and the + operator works on your objects:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
# Return the same type, so chaining works
return Vector(self.x + other.x, self.y + other.y)
v = Vector(1, 2) + Vector(3, 4) + Vector(1, 1)
print(v.x, v.y) # 5 7
Without __add__, v1 + v2 raises TypeError.
Containers: __len__ and __contains__#
These two make your class behave like a built-in collection:
class Library:
def __init__(self):
self.books = []
def __len__(self):
return len(self.books)
def __contains__(self, book):
return book in self.books
lib = Library()
lib.books.append("Harry Potter")
print(len(lib)) # 1
print("Harry Potter" in lib) # True
print("Game of Thrones" in lib) # False
Without __len__, len() raises TypeError. Without __contains__, in falls back to iterating (__iter__ or __getitem__) and raises TypeError if neither exists.
Cheat sheet#
The dunders you'll use most often:
| Dunder | Triggered by | Default for your class |
|---|---|---|
__new__ |
Dog() |
Creates the object |
__init__ |
Dog() |
Does nothing |
__str__ |
print, str() |
Falls back to __repr__ |
__repr__ |
repr(), REPL |
<Dog object at 0x...> |
__eq__ |
== |
Identity comparison |
__lt__ |
<, sort() |
TypeError |
__add__ |
+ |
TypeError |
__len__ |
len() |
TypeError |
__contains__ |
in |
Iterates if possible, else TypeError |
@property: computed attributes#
@property turns a method into something you read like an attribute. The value is computed on every access, not stored in the instance __dict__, so it stays in sync with the data it depends on.
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return math.pi * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(round(c.area, 1)) # 78.5, no () needed
c.radius = 10
print(round(c.area, 1)) # 314.2, updated automatically
For validated setters, see Encapsulation.
Where things live#
d = Dog("Bruno", 3)
print(d.__dict__) # {'name': 'Bruno', 'age': 3}: instance variables only
print(Dog.__dict__) # methods, class variables, properties
| What | Stored in |
|---|---|
| Instance variables | obj.__dict__ |
| Methods and class variables | Class.__dict__ |
@property |
Class.__dict__, as a descriptor (not a value) |
| Instance variable | @property |
|
|---|---|---|
In instance __dict__? |
Yes | No |
| Value | Stored | Computed on access |
| Updates when inputs change? | No | Yes |
| Use when | The value is independent | The value derives from other attributes |
Key takeaways#
- Python calls dunder methods for you when you use operators and built-ins.
- Define
__repr__at minimum;__str__is for friendly output. - Default
==is identity; define__eq__(and__hash__if needed) for value equality. - Return your own type from arithmetic dunders so expressions chain.
@propertyvalues are computed on access and live on the class as descriptors.