Skip to main content

Dunder methods & properties

Level 2: Level 2: Idiomsmedium12 mindunder-methodseqpropertyclasses

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
Check yourself
Suppose Point defines __init__ and __repr__ but no __eq__ at all. What is Point(1, 2) == Point(1, 2)?

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
Check yourself
Circle exposes area as an @property. A caller who is used to methods writes Circle(2).area(). What happens?

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.x raises AttributeError when other has no .x (for example Point(1, 2) == "hi"). Guard with isinstance and return NotImplemented so Python falls back to a safe False instead of blowing up.
  • Calling a property. A @property is read like data (circle.area), never called (circle.area()).
  • Unhashable objects. Defining __eq__ sets __hash__ to None, 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).
Check yourself
Your Point class worked fine as a dict key. You add an __eq__ that compares x and y. What else changes?

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

Worked example (Python)
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

Apply

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.