Execute Program

Python in Detail: Customizing Equality

Welcome to the Customizing Equality lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • In past lessons, we used == to check equality and the is operator to check identity. When we define a new class, equality checks with == behave exactly like identity checks with is. In other words, our objects are only equal to themselves.

  • >
    class Cat:
    def __init__(self, name):
    self.name = name
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    cat1 = Cat("Keanu")
    (cat1 == cat1, cat1 is cat1)
    Result:
    (True, True)Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    cat1 = Cat("Keanu")
    cat2 = Cat("Ms. Fluff")
    (cat1 == cat2, cat1 is cat2)
    Result:
    (False, False)Pass Icon
  • This makes intuitive sense: every cat object is itself, and is equal to itself. But this rule also means that no two cat objects are ever equal to each other, even if they have the same attributes.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    cat1 = Cat("Keanu")
    cat2 = Cat("Keanu")
    (cat1 is cat2, cat1 == cat2)
    Result:
    (False, False)Pass Icon
  • Often, we want two different objects to be equal when their attributes are the same, like the two Keanus above. To achieve that, we need to override the default equality behavior.

  • When we check point_a == point_b, Python actually calls point_a.__eq__(point_b). We can define our own .__eq__ to customize how Points are compared for equality.

  • The .__eq__ dunder method takes one argument: the object we're comparing to, often named other. It returns a boolean, which determines the outcome of the comparison. In the next example, two Points are equal if their x and y coordinates are equal.

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

    def __eq__(self, other):
    return self.x == other.x and self.y == other.y
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point_a = Point(5, 3)
    point_b = Point(5, 3)
    point_a == point_b
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point_a = Point(5, 3)
    point_b = Point(5, 3)
    point_a is point_b
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point_a = Point(5, 3)
    point_c = Point(4, 4)
    point_a == point_c
    Result:
    FalsePass Icon
  • Note that comparisons generally shouldn't raise exceptions. For example, if we compare two built-in values that can't possibly be equal, we don't get a TypeError. Instead, we get False because the values are different.

  • >
    "Hello" == 1
    Result:
    FalsePass Icon
  • Unfortunately, our current Point class breaks this convention. The .__eq__ method always tries to access other.x and other.y, even if other doesn't have those attributes. Accessing an attribute that doesn't exist raises an AttributeError exception.

  • (You can type "error" when a code example will cause an error.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point_a = Point(5, 3)
    point_a == "Hello"
    Result:
    AttributeError: 'str' object has no attribute 'x'Pass Icon
  • The usual solution to this problem is to check the type of other using isinstance. If it's not a Point, we return False.

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

    def __eq__(self, other):
    if not isinstance(other, Point):
    return False
    return self.x == other.x and self.y == other.y
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Point(5, 3) == Point(5, 3)
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Point(5, 3) == Point(1000, 2000)
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Point(5, 3) == "Hello"
    Result:
    FalsePass Icon
  • This works, but sometimes we do want to compare different classes. For example, we might have a Vector class with xComponent and yComponent attributes. A Point and a Vector should be equal if point.x == vector.xComponent and point.y == vector.yComponent.

  • Since these attributes have different names, Vector.__eq__ needs to check other's type, handling Points and Vectors separately.

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

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

    class Vector:
    def __init__(self, x, y):
    self.xComponent = x
    self.yComponent = y

    def __eq__(self, other):
    if isinstance(other, Point):
    return self.xComponent == other.x and self.yComponent == other.y
    elif isinstance(other, Vector):
    return (
    self.xComponent == other.xComponent
    and self.yComponent == other.yComponent
    )
    else:
    return False
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point = Point(5, 3)
    vector = Vector(5, 3)

    vector == point
    Result:
    TruePass Icon
  • That worked, but unfortunately there's a bug. If we compare in the other direction, we get the wrong answer.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point == vector
    Result:
    FalsePass Icon
  • Equality should be symmetric: if a == b is true then b == a should also be true. Unfortunately Python can't ensure symmetry for us; it's our job to ensure it.

  • The problem is that we customized both classes' .__eq__ methods. When we compare vector == point, we're calling vector.__eq__(point), which works correctly. When we compare point == vector, we're calling point.__eq__(vector), but point.__eq__ doesn't know about vectors.

  • Fortunately, Python has an elegant solution to this problem: NotImplemented. This is a special value that we return when .__eq__ encounters a type that it doesn't support.

  • First, here's the working code. Then we'll discuss why NotImplemented solves the problem.

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

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

    class Vector:
    def __init__(self, x, y):
    self.xComponent = x
    self.yComponent = y

    def __eq__(self, other):
    if isinstance(other, Point):
    return self.xComponent == other.x and self.yComponent == other.y
    elif isinstance(other, Vector):
    return (
    self.xComponent == other.xComponent
    and self.yComponent == other.yComponent
    )
    else:
    return NotImplemented
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point = Point(5, 3)
    vector = Vector(5, 3)

    vector == point
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    point == vector
    Result:
    TruePass Icon
  • When we compare point == vector, Python calls Point.__eq__(point, vector). That returns NotImplemented because other isn't a point.

  • NotImplemented tells Python that our dunder method isn't implemented for that type, and that Python should try the operation in a different way if possible. For the == operator, Python will try flipping the operands. Because point == vector is NotImplemented, Python tries vector == point. This calls Vector.__eq__(vector, point), which returns True.

  • Note that this only happens if we return NotImplemented. If we return False, like we did in our original code, then we might get objects where a == b is not the same as b == a.

  • Here's a code problem:

    We're working on a system for pet hotels where Owners have Cats. Owners have an email and an id. Cats have a name and an id.

    Owners can change their email addresses, but their IDs are the ultimate marker of their identity. Two owner objects with the same ID should be equal even when their email addresses are different. Add an .__eq__ method to the Owner class that compares owners by id.

    Note that an Owner and a Cat might happen to have the same id. Make sure that Owners can only be equal to other Owners! (A hint: you can use isinstance for this.)

    class Cat:
    def __init__(self, name, id):
    self.name = name
    self.id = id

    class Owner:
    def __init__(self, email, id):
    self.email = email
    self.id = id

    def __eq__(self, other):
    if not isinstance(other, Owner):
    return NotImplemented
    return self.id == other.id
    # Two Amirs with different email addresses, but they're the same user because
    # the IDs match.
    amir = Owner("amir@example.com", 1)
    amir2 = Owner("amir+nospam@example.com", 1)

    # Two people named Betty with the same email address, but they're different
    # users because the IDs are different.
    betty = Owner("betty@example.com", 2)
    betty2 = Owner("betty@example.com", 3)

    keanu = Cat("Keanu", 1)

    (amir == amir2, betty == betty2, amir == keanu)
    Goal:
    (True, False, False)
    Yours:
    (True, False, False)Pass Icon
  • There are several other comparison methods in addition to .__eq__. Each of these is customizable like .__eq__.

    • __ne__ is "not equal", !=.
    • __lt__ is "less than", <.
    • __le__ is "less than or equal to", <=.
    • __gt__ is "greater than", >.
    • __ge__ is "greater than or equal to", >=.
  • It's easy to look these up, so it's not important to memorize their names. Even if you don't memorize them, you may be able to guess them in the future by remembering that each comparison method's name contains exactly two letters.

  • Note that all of these dunder methods work with NotImplemented. For example, if a < b returns NotImplemented, Python will try b > a.

  • Customizing .__eq__ is quite common in practice. Often, we want to compare our instances by equality, rather than using the simplistic identity comparison that we get by default. In many cases, equality is more subtle than "all of the attributes are the same." For example, in the code problem above, our Owner objects only compared via IDs, ignoring email addresses.

  • Which attributes we compare depends on the system, and how we think of the identity of individual objects. Unfortunately, that means that Python can never do it for us. But on the bright side, .__eq__ and NotImplemented are robust tools for implementing equality.