Dunder methods & properties
Give classes natural behaviour with __eq__/__repr__ and computed @property values.
Why give a class built-in behaviour
Print a plain object and you get <__main__.Point object at 0x10f3c2a90>. Compare two of them with == and you get False unless they are literally the same object in memory. That is useless in tests, logs, and debugging. Dunder methods let your class plug into the same protocols the built-in types use, so ==, print(), len(), [], and more behave the way callers expect.
Dunder methods: hooking into Python's protocols
A dunder ("double underscore") method has a name like __eq__. Python calls it for you when the matching syntax runs. a == b calls a.__eq__(b), and repr(a) (and print(a), when no __str__ exists) calls a.__repr__().
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
print(Point(1, 2)) # Point(1, 2)
print(Point(1, 2) == Point(1, 2)) # True
print(Point(1, 2) == Point(3, 4)) # False
By default == compares identity (the same check as is), so two freshly built points are unequal. Defining __eq__ replaces that with value equality: compare the coordinates that actually matter.
Computed attributes with @property
A @property turns a method into a read-only attribute, accessed without parentheses. Reach for it when a value is derived from other fields and you do not want to store it or recompute it by hand.
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
print(Circle(2).area) # 12.56636
Circle(2).area runs the method and hands back the number. Writing Circle(2).area() would raise TypeError: 'float' object is not callable, because area already returned a float.
Pitfalls
__eq__that crashes on foreign types.self.x == other.xraisesAttributeErrorwhenotherhas no.x(for examplePoint(1, 2) == "hi"). Guard withisinstanceandreturn NotImplementedso Python falls back to a safeFalseinstead of blowing up.- Calling a property. A
@propertyis read like data (circle.area), never called (circle.area()). - Unhashable objects. Defining
__eq__sets__hash__toNone, so instances can no longer be dict keys or set members. Add a matching__hash__(equal objects must hash equal) or use@dataclass(frozen=True).
Interview nuance: the hash invariant says objects that compare equal must return the same hash(). That is exactly why Python drops __hash__ the moment you define __eq__: keeping the old identity-based hash would let two equal points land in different buckets and quietly break set and dict lookups. If you do make a value type hashable, base both __eq__ and __hash__ on the same fields (here, (self.x, self.y)).
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
print(Circle(2).area) # 12.56636Apply
Your turn
The task this lesson builds to.
Implement Point.__eq__ so two points are equal when both coordinates match.
run(1, 2, 1, 2) should return True; run(1, 2, 3, 4) should return False.
2 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Add an area @property to Circle that returns π · r² (use 3.14159 for π).
run(2) should return 12.56636. Access it as circle.area (no parentheses).
3 hints and 4 automated checks are waiting in the workspace.