itsakettle

#2 | Equality in Python

Overview

In python3:

Examples

Objects from custom classes

Say we have a circle class and we create two instances.

class Circle:
    def __init__(self, color):
        self.color = color

blue_circle_one = Circle("blue")
blue_circle_two = Circle("blue")

Now let’s see what happens when we use is and == to compare the two circles.

print(blue_circle_one is blue_circle_two) # False
print(blue_circle_one == blue_circle_two) # False

We get False from is because a and b point to different memory addresses and therefore two different objects. We get False from == because the values of a and b are different. Hold on how does it assign a ‘value’ to an object? Well the default is id(object) which just gives the memory address. So the check for equality here is the exact same for is and ==.

But we don’t have to use the default value for ==.

class Circle:
    def __init__(self, color):
        self.color = color

    def __eq__(self, otherCircle):
        if isinstance(otherCircle, Circle):
            return otherCircle.color == self.color
        else:
            return NotImplemented
        
blue_circle_one = Circle("blue")
blue_circle_two = Circle("blue")

print(blue_circle_one is blue_circle_two) # False
print(blue_circle_one == blue_circle_two) # True

Now we get True for == because we’ve implemented the __eq__ function to say that the circles are equal if their color is the same. The __eq__ function of the object on the left hand side of == is used and if that returns NotImplemented then the __eq__ function of the right hand object is used. If both return NotImplemented then False is returned overall.

Note also that there is nothing that forces __eq__ to return a boolean. For example in the code below circle == square returns apple.

class Circle:
    def __init__(self, color):
        self.color = color

    def __eq__(self, otherCircle):
        return "apple"
        
class Square:
    def __init__(self, color):
        self.color = color

    def __eq__(self, otherSquare):
        if isinstance(otherSquare, Square):
            return otherSquare.color == self.color
        else:
            return NotImplemented

circle = Circle("blue")
square = Square("blue")

print(circle == square) # apple

Built in types

For built in types such as int, float and list the value used by == to determine equality is predefined and just makes sense.

a = 1
b = 2
print(a == b) # False

a = [1, 2]
b = [1, 2, 3]
print(a == b) # False

a = "a"
b = "a"
print(a == b) # True

Let’s look at the behaviour of is.

a = 1
b = 2
print(a is b) # False

a = [1, 2]
b = [1, 2, 3]
print(a is b) # False

a = "a"
b = "a"
print(a is b) # True !!!

A slight surprise is that when a and b are assigned the same value ("a" in this case) then is returns True which doesn’t seem to make sense because surely a seperate object is created each time. Let’s check.

a = "a"
b = "a"
a is b # True !!!

print(id(a)) # 4450444768
print(id(b)) # 4450444768

Nope, a and b are pointing to the same object. This seems to be an efficiency in Python where it tries not to create the same built in type twice.

Checking for None

At first it looks like x == None works.

x = 1
print(x == None) # False

x = None
print(x == None) # True

This is because None is an object in memory and id(None) returns a memory address.

However, as we saw above, __eq__ can return anything and in particular it could always return True, say. Here’s an example.

class Circle:
    def __init__(self, color):
        self.color = color

    def __eq__(self, otherCircle):
        return True

circle = Circle("blue")
print(circle == None) # True !!!

So when checking for None using == carries a small risk and it’s safer to use is.