Embercode
✕
Object-Oriented ProgrammingLesson 10 of 17

45 min read

Operator overloading with __eq__, __hash__, __lt__ and __str__

Why this matters

Programs compare things all the time. Is the password the one we stored? Is this product already in the cart? Is this customer the same one as before? Programs also print things, such as an order or a price, so a person can read them.

By default, a class you write does not know how to answer any of these questions. In Python, == on two objects of your own class checks whether they are the same object in memory, which is often not what you want. The operator + does not work on your objects at all, and sorted does not know how to order them.

This lesson shows how to tell Python what "equal", "less than", "plus" and "printed form" mean for your own class. Python calls these operators through special methods, whose names start and end with two underscores. The lesson uses one small example, a Money class that holds an amount in cents, and builds it up. The lesson on immutable objects, which comes just before this one, explains why a value like Money is a good fit for equality checks.

The default: two objects are two identities

Start with a class that does nothing special.

Python · editable

The output is:

a == b: False
a is b: False

Both objects hold 1050 cents, yet Python says they are not equal. The reason is that a and b are two separate objects in memory. For a class that does not define __eq__, the operator == falls back to the same check as is. The is operator asks whether two names point to the same object. The == operator asks whether the values are equal, and it uses the special method __eq__ to answer. Without one, the answer is the identity check.

To make == compare the amounts instead, the class has to define __eq__. The next sections do that.

Operators are method calls

In Python, a == b is shorthand for a call to a.__eq__(b). The expression a + b calls a.__add__(b). The expression a < b calls a.__lt__(b). The built-in functions do the same: len(a) calls a.__len__(), and print(a) calls str(a), which calls a.__str__(). A class can define any of these special methods to change what the operator does for its objects. This is called operator overloading. "Overloading" means giving an existing operator a new meaning for a new type.

Here is the Money class with the methods it needs. Each one is explained after the example.

Python · editable

The output is:

a = $10.50
a == b: True
a != c: True
a + c = $13.00
a is b: False

Some details in this code need explaining.

  • __eq__ takes the other object as a parameter and returns True or False. The check isinstance(other, Money) makes sure the other value is also a Money. If it is not, the method returns NotImplemented. That special value tells Python to try the other side of the comparison. Returning it, rather than False, lets another class decide.
  • != needs no method of its own. Python builds it from __eq__. If two values are not equal, they are different, so the rule is written only once.
  • __add__ builds and returns a new Money. It does not change a or c, which is the copy on change pattern from the immutable objects lesson.
  • __repr__ gives the text form for developers, such as the list output shown later. __str__ gives the text form for people, and print uses it. If a class defines only __repr__, print falls back to it.
  • The format {self._cents % 100:02d} pads the cents to two digits with a leading zero, so 5 cents prints as 05.

Comparing with other types

The __eq__ method returns NotImplemented for values of another type. Here is what that means in practice:

Python · editable

The output is:

False
True

A Money never equals a string, even when the text looks the same. Python tries Money.__eq__, gets NotImplemented, then tries the string's side, which also says no. The result is False. Returning False directly would also work for this class, but returning NotImplemented is the standard choice, because it lets the other type take part in the comparison.

Ordering: __lt__ and sorted

Python sorts a list by comparing its items with <. A class that defines __lt__, which means "less than", can be sorted with sorted and list.sort, and it can be used with min and max. The next example sorts a list of money values.

Python · editable

The output is:

[Money(120), Money(500), Money(900)]
Money(120) Money(900)

The function sorted returns a new list, and the original list is not changed. Python's sort uses only <, so __lt__ is enough for it. The square brackets show the repr text, because a list displays its items with repr.

When you need >, <= and >= as well, write them by hand, or use functools.total_ordering. That decorator, a line starting with @ that changes the class below it, fills in the missing comparison methods when you write __eq__ and one ordering method.

Python · editable

The output is:

True
False

Neither >= nor > is written in the class. The decorator builds them from __eq__ and __lt__.

A tuple compares its items from left to right, and it is a convenient way to write a rule with several parts. For example, (self.year, self.title) < (other.year, other.title) sorts by year first and by title when the years match, as the interfaces lesson showed.

Hashing: the rule that links __eq__ and __hash__

Python sets and dictionaries use a hash code to find where an object is stored. A hash code is a whole number that the object gives for itself. Python uses it to pick a small group of stored objects to look at, and then it uses == to find the match inside that group.

The rule that joins the two methods is this. If two objects are equal, they must have the same hash code. The reverse is not required, because two different objects can share a hash code. Python makes sure that a class with __eq__ and no __hash__ cannot be stored in a set or used as a dictionary key. The next example shows the error:

Python · editable

The output is:

TypeError: cannot use 'NoHash' as a set element (unhashable type: 'NoHash')

When a class defines __eq__, Python sets its __hash__ to None unless the class also defines __hash__. The message says the class is unhashable. The fix is to define __hash__ with the same fields that __eq__ uses:

Python · editable

The output is:

set size: 1
contains c-7: True

Two Key objects with the same id are equal by __eq__, and they have the same hash code, so the set keeps one of them. A lookup with a new object finds it.

If __eq__ and __hash__ disagreed, the set would hold two copies of the same key, or would fail to find one that it holds. The rule that equal objects must share a hash code is what keeps the two in step.

A key that changes after it is stored

A set also depends on the hash code not changing while the object sits inside it. Here is a key whose field can be changed.

Python · editable

The output is:

before change: True
after change, contains a1: False
after change, contains b2: False

The object was stored in a group chosen by the hash code of "a1". After k.item_id changed to "b2", its hash code changed too, so a lookup now looks in a different group and does not find it. The object is still inside the set, but the set cannot find it. The common fix is to make the fields used by __eq__ and __hash__ read-only, for example with a frozen dataclass, so they cannot change after the object is built.

Text forms: __str__ and __repr__

Python has two text forms. str is for people, and print uses it. repr is for developers, and it should show enough detail to rebuild the object. The interactive prompt and the list display use repr. Both are optional. If you define only __repr__, Python uses it for both.

Python · editable

The output is:

(4, -2)
[Point(4, -2)]
Point(4, -2)

The first line uses __str__, because print calls str. The second line shows a list, which uses repr for each item. The third line uses !r in the f-string to ask for the repr form on purpose.

A value class with a full set of operators

Putting the pieces together, a small value class looks like this. A frozen dataclass can write most of this for you, as the immutable objects lesson showed. The version below writes each method by hand so that you can see what each one does.

Python · editable

The output is:

distinct amounts: 2
sorted: [Money(250), Money(1050)]
total: $13.00
text: $0.05

The set holds two items, because the two Money(1050) objects are equal and share a hash code. The sort uses __lt__. The + uses __add__. The text form for people comes from __str__.

Java and C++ comparison

Java and C++ use different names for the same jobs. In Java, a class overrides equals, hashCode and toString, and Comparable provides ordering. In C++, the class defines operator==, operator< and operator<<. Python uses the special methods __eq__, __hash__, __lt__ and __str__, which play the same roles. One difference matters most. In Java, equals exists on every object, and the default compares identity. In Python, == also compares identity by default, and a class that defines __eq__ without __hash__ loses the ability to be stored in a set.

Common mistakes

Defining __eq__ without __hash__

The NoHash example above shows the result. A class with __eq__ and no __hash__ cannot be used in a set or as a dictionary key. The message ends with unhashable type: .... Define __hash__ with the same fields, or make the class a frozen dataclass.

Using is to compare values

The is operator checks whether two names point to the same object. For numbers, text and your own value objects, use ==. Use is for None, as in if value is None, because None is a single shared object.

Python · editable

The output of the first line is False, because Money(1050) builds a new object. The second line is True.

Returning False for a different type in a way that hides a bug

Returning False directly from __eq__ for every other type is simple, and it works for most classes. The risk is that a comparison with a value of the wrong type hides a bug quietly. The standard choice is NotImplemented, which lets Python report a TypeError when no other side can answer an ordering comparison, such as <.

Forgetting __lt__ before sorting

A list of your own objects cannot be sorted until the class defines __lt__. Python raises an error that names the two types:

Python · editable

The output is:

TypeError: '<' not supported between instances of 'Book' and 'Book'

The fix is to define __lt__ on the class, or to pass a key function to sorted, such as sorted(books, key=lambda b: b.title).

Printing an object and seeing only its address

A class without __str__ or __repr__ shows the default text, which includes the class name and an address in memory. The address is different on every run and on every machine, so the output cannot be checked. Define __repr__ for any class you print.

Debugging walkthrough: two equal objects are treated as different

Suppose a set holds two objects that look the same, or a lookup fails for an object that is in the set. Use this routine.

  1. Check that the class defines __eq__. If it does not, == compares identity, and two separate objects are never equal.
  2. Check that __eq__ compares the fields that define the value, and that it returns NotImplemented or False for other types.
  3. Check that __hash__ is defined, and that it uses the same fields as __eq__.
  4. Check that the fields used by __hash__ do not change while the object is in a set or a dictionary.
  5. Print the hash codes of the two objects, with hash(a) and hash(b). Equal objects must print the same number.

Variations and edge cases

Python lets an operator work with a value on the left or the right. The method __radd__ handles 5 + money, when the left value does not know how to add. This is rarely needed in value classes, but it is how libraries such as numeric arrays combine with plain numbers.

A class can define __bool__ to say when an object counts as true, such as an empty collection. Without it, Python uses __len__ if the class defines one, and otherwise every object counts as true.

A class can define __contains__ so that x in obj works. A class can define __len__ so that len(obj) works. These follow the same pattern as the operators above.

Edge case: a comparison with None. Money(5) == None returns False with the __eq__ above, because None is not a Money. If a value might be None, check for it before you call a method on it.

Interview-style questions

What is the difference between == and is in Python? == asks whether two values are equal, and it calls __eq__. is asks whether two names point to the same object.

What must hold between __eq__ and __hash__? Two equal objects must have the same hash code. A class that defines __eq__ must also define __hash__ to be used in a set or as a dictionary key.

Why does a class that defines __eq__ lose its __hash__? Python sets __hash__ to None to stop the class from being used in a set when the rule above might be broken. Defining __hash__ with the same fields restores it.

What does NotImplemented do in __eq__? It tells Python that this class cannot answer, so Python tries the other side. It is different from False, which is a final answer.

What is the difference between __str__ and __repr__? __str__ is the text for people, and print uses it. __repr__ is the text for developers, and lists and the interactive prompt use it.

How does sorted compare two objects? It uses <, which calls __lt__. A class that defines only __lt__ is enough for sorted, min and max.

Real-world uses

Value classes such as money, dates, coordinates and identifiers need equality and text forms. A payments library compares amounts with == and sums them with +, and it prints them with __str__ in reports. A geometry library defines + and - for vectors, so the code reads like the maths. A cache or a deduplication step stores objects in a set, and it depends on __eq__ and __hash__ agreeing.

Practice

Exercise 1

Write a class Point with two attributes, x and y. Define __eq__, __hash__ and __repr__ so that Point(1, 2) prints as Point(1, 2), two points with the same x and y are equal, and a set holding two such points has size 1.

Show the answer

Python · editable

The output is Point(1, 2), then True, then 1. The hash uses a tuple of the same two fields that __eq__ compares.

Exercise 2

The class BadKey below has an __eq__ but no __hash__. Add the missing method so that a set of two keys with the same id has size 1. Do not change __eq__.

class BadKey:
    def __init__(self, item_id):
        self.item_id = item_id
 
    def __eq__(self, other):
        return isinstance(other, BadKey) and self.item_id == other.item_id

Show the answer

Add this method inside BadKey:

def __hash__(self):
    return hash(self.item_id)

With the method added, the two keys share a hash code, and the set size is 1.

Exercise 3

Write a class Money with __lt__ so that a list of three Money values holding 500, 120 and 900 cents prints in order from smallest to largest. Use the cents value only, and print each amount on its own line.

Show the answer

Python · editable

The output is 120, then 500, then 900.

Exercise 4

Write a __str__ method for a Money class that holds cents, so that Money(5) prints as $0.05, and Money(1050) prints as $10.50.

Show the answer

Python · editable

The output is $0.05, then $10.50. The format :02d pads the cents to two digits with a leading zero.

Exercise 5

A class Vector holds x and y. Define __add__ so that Vector(1, 2) + Vector(3, 4) gives a new Vector(4, 6), and define __repr__ so that the result prints as Vector(4, 6). Check that the original vectors do not change.

Show the answer

Python · editable

The output is Vector(4, 6), then Vector(1, 2) Vector(3, 4). The method __add__ builds a new object, so a and b keep their values.

Summary

  • Python operators call special methods. == calls __eq__, + calls __add__, < calls __lt__, and print calls __str__.
  • Without __eq__, == compares identity, so two separate objects are never equal.
  • A class that defines __eq__ must define __hash__ with the same fields, or it cannot be used in a set or as a dictionary key.
  • Return NotImplemented from __eq__ for other types, so Python can try the other side.
  • sorted, min and max use __lt__. functools.total_ordering fills in the other comparisons.
  • __str__ is the text for people, and __repr__ is the text for developers.
Check yourself: Immutable and equality (8 questions) →Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.