Overloading equality in Python

Share
Copied to clipboard.
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Watch as video Python 3.8—3.12

In Python, you can customize what happens when you ask objects whether they're equal to each other.

The default implementation for equality is the same as identity

Here we have a Point class:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

And we have two Point objects (two instances of the Point class):

>>> a = Point(1, 2)
>>> b = Point(1, 2)

If we ask these two objects whether they're equal, Python tells us False, even though they have the same attributes:

>>> a == b
False

But if we ask one object whether it's equal to itself, Python tells us True:

>>> a == a
True

The default implementation for equality in Python does the same thing as identity.

So we were really asking, are these two objects exactly the same object in memory:

>>> a is b
False
>>> a is a
True

How does the == operator work behind the scenes in Python?

If we understand how the == operator works in Python, we can customize what happens when we ask two objects whether they're equal.

When == is used between two objects, Python calls the __eq__() method on the first object, passing in the second object.

>>> a.__eq__(b)
NotImplemented

If it gets back True or False, it returns that value back to us.

But if it gets NotImplemented, it then goes to the second object and asks whether it's equal to the first object:

>>> b.__eq__(a)
NotImplemented

If it gets True or False at this point, it returns that value back to us. But if it gets NotImplemented this time, it returns False, because neither of these two objects know how to compare themselves to the other object.

Because a doesn't know how to compare itself to b and b doesn't know how to compare itself to a, these two values are not equal:

>>> a == b
False

Customizing how equality works with the __eq__ method

If we wanted to implement equality on our Point class (to customize what it means for Point objects to be equal to other objects) we could make a __eq__ method in our Point class:

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):
        return (self.x, self.y) == (other.x, other.y)

Our __eq__ method needs to accept self and another object to compare to (we'll call it other). Our __eq__ method will make a tuple of our x and y attributes and compare it to the same tuple for the other object:

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

If our object's x and y attributes are equal to the other object's x and y attributes, then these two objects will be seen as equal.

So if we take two Point objects (a and b) that have the same x and y values:

>>> a = Point(1, 2)
>>> b = Point(1, 2)

And we ask Python whether they're equal, we'll now get True:

>>> a == b
True

But if we compare two Point objects that don't have the same x and y values, we'll get False:

>>> b = Point(1, 2)
>>> c = Point(1, 3)
>>> b == c
False

But we're not done yet!

Using type-checking in equality to ensure that the two objects are comparable

Our __eq__ method is not very well-behaved right now. If we pass in an object that isn't a Point object, something weird happens.

For example if we passed in None, we get an AttributeError:

>>> a == None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/trey/point.py", line 10, in __eq__
    return (self.x, self.y) == (other.x, other.y)
AttributeError: 'NoneType' object has no attribute 'x'

We're getting an AttributeError because that other type of object doesn't have x and y attributes.

There's a number of ways to solve this problem, but the recommended way is to rely on type-checking. If other is an instance of our Point class, we'll compare other and self as we did before. But other isn't a Point, we'll return NotImplemented:

    def __eq__(self, other):
        if isinstance(other, Point):
            return (self.x, self.y) == (other.x, other.y)
        return NotImplemented

This might seem a little bit weird because it's uncommon to see type-checking used in Python (we usually practice duck typing instead). But inside dunder methods is one of the few places where type-checking is deemed acceptable.

Inside our __eq__ method, we really need to ask ourselves: do we know how to compare ourselves to that other object? If we don't, we should go delegate to that other object instead (and that's what NotImplemented is for).

So now if we compare our Point objects to None, we get False (as expected):

>>> a == None
False

Summary

If you'd like to customize what it means for instances of your class to be equal to some other object, you can define a __eq__ method. But make sure that that your __eq__ method returns NotImplemented when given an object that you don't know how to compare yourself to.

A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.