Skip to content
aviral gupta

// A1.1 · ~30 min · Advanced

__repr__, __str__, __eq__ and __hash__

After this lesson you can give a class a helpful repr and str, compare its objects by value, and make them safe set members and dict keys.

Lesson 1 of 6 in A1 The data model

Start of the module

You will be able to

  • Write __repr__ and __str__, and predict which one print, f-strings and containers use
  • Write __eq__ and __hash__ that keep the rule: equal objects have equal hashes
  • Explain why a class with __eq__ alone is unhashable, and why a hash must not change
  1. Warm-up · Activity 1 of 7

    Warm-up from module I2: which methods does a plain @dataclass (no arguments) write for you? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: the class defines both __repr__ and __str__. What does this print?

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
        def __str__(self):
            return f"({self.x}, {self.y})"
    
    
    p = Point(1, 2)
    print(p, [p])
  3. Practice · Activity 3 of 7

    Fill in the built-in so that the hash is built from the same fields that __eq__ compares.

    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, self.y) == (other.x, other.y)
    
        def __hash__(self):
            return ____((self.x, self.y))
    return ((self.x, self.y))
  4. Practice · Activity 4 of 7

    Money defines __eq__ but no __hash__. What does this print?

    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __eq__(self, other):
            return isinstance(other, Money) and self.cents == other.cents
    
    
    print(Money(5) == Money(5), Money.__hash__)
  5. Practice · Activity 5 of 7

    The class defines both __repr__ and __str__. Match each expression to the method that produces its text.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. The ticket is put in a set, then its number changes. What does this print?

    class Ticket:
        def __init__(self, number):
            self.number = number
    
        def __eq__(self, other):
            return isinstance(other, Ticket) and self.number == other.number
    
        def __hash__(self):
            return hash(self.number)
    
    
    t = Ticket(1)
    open_tickets = {t}
    t.number = 2
    print(t in open_tickets, Ticket(1) in open_tickets)
  7. Apply · Activity 7 of 7

    Mini-task. Write a class Money(cents, currency). repr gives Money(250, 'EUR'), str gives 2.50 EUR, two objects are equal when cents and currency match, and a set of Money(250, "EUR"), Money(250, "EUR") and Money(250, "USD") has two items.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Stock-keeping units as dict keys

A shop scans items as product code plus size. Codes arrive in any case, so Sku stores them in upper case and compares by value. With __eq__ and __hash__ on the same two fields, the duplicate scan collapses in a set, and a freshly built Sku finds its stock count in a dict. repr shows in the list, str in the report lines.

main.py

class Sku:
    """A stock-keeping unit: product code plus size, compared by value."""

    def __init__(self, code: str, size: str) -> None:
        self.code = code.upper()
        self.size = size

    def __repr__(self) -> str:
        return f"Sku({self.code!r}, {self.size!r})"

    def __str__(self) -> str:
        return f"{self.code}-{self.size}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Sku):
            return NotImplemented
        return (self.code, self.size) == (other.code, other.size)

    def __hash__(self) -> int:
        return hash((self.code, self.size))


scanned = [Sku("tee", "M"), Sku("TEE", "M"), Sku("cap", "L")]
print("Scanned:", scanned)
print("Unique items:", len(set(scanned)))

stock = {Sku("TEE", "M"): 12, Sku("CAP", "L"): 3}
for sku in scanned:
    print(f"{sku}: {stock[sku]} in stock")

print(Sku("tee", "M") == "TEE-M")

Run it with

python main.py

Output

Scanned: [Sku('TEE', 'M'), Sku('TEE', 'M'), Sku('CAP', 'L')]
Unique items: 2
TEE-M: 12 in stock
TEE-M: 12 in stock
CAP-L: 3 in stock
False
  • The list prints each Sku with __repr__; the f-string lines use __str__.
  • Sku("tee", "M") and Sku("TEE", "M") are equal and hash the same, so the set keeps one.
  • stock[sku] works with a different object than the key stored, because the hash and == agree.
  • Comparing with a string returns NotImplemented from __eq__, and Python falls back to identity: False.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Two faces of a colour

Give the class Color in main.py a __repr__ and a __str__. repr(Color(255, 128, 0)) must be Color(255, 128, 0), and str() the hex code #ff8000, with two lower-case hex digits per channel.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Both methods take only self and must return a str, not print it.

  2. Hint 2

    The format spec 02x writes an int as two lower-case hex digits: f"{10:02x}" is 0a.

  3. Hint 3

    return f"#{self.red:02x}{self.green:02x}{self.blue:02x}" for __str__, and f"Color({self.red}, {self.green}, {self.blue})" for __repr__.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Color:
    """An RGB colour with channels from 0 to 255."""

    def __init__(self, red: int, green: int, blue: int) -> None:
        self.red = red
        self.green = green
        self.blue = blue

    def __repr__(self) -> str:
        return f"Color({self.red}, {self.green}, {self.blue})"

    def __str__(self) -> str:
        return f"#{self.red:02x}{self.green:02x}{self.blue:02x}"


if __name__ == "__main__":
    orange = Color(255, 128, 0)
    print(repr(orange), orange)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Color:
    """An RGB colour with channels from 0 to 255."""

    def __init__(self, red: int, green: int, blue: int) -> None:
        self.red = red
        self.green = green
        self.blue = blue

    # Add __repr__: Color(255, 128, 0)
    # Add __str__: #ff8000


if __name__ == "__main__":
    orange = Color(255, 128, 0)
    print(repr(orange), orange)

test_main.py

from main import Color


def test_repr():
    """repr() looks like the call that builds the colour"""
    got = repr(Color(255, 128, 0))
    assert got == "Color(255, 128, 0)", f"repr(Color(255, 128, 0)) returned {got!r}, expected 'Color(255, 128, 0)'"


def test_str():
    """str() is the hex code with two digits per channel"""
    got = str(Color(255, 128, 0)), str(Color(0, 10, 255))
    assert got == ("#ff8000", "#000aff"), f"str() returned {got!r}, expected ('#ff8000', '#000aff')"


def test_print_uses_str():
    """An f-string uses __str__"""
    got = f"colour: {Color(0, 0, 0)}"
    assert got == "colour: #000000", f"the f-string gave {got!r}, expected 'colour: #000000'"


def test_list_uses_repr():
    """A list shows its items with __repr__"""
    got = str([Color(1, 2, 3)])
    assert got == "[Color(1, 2, 3)]", f"str([Color(1, 2, 3)]) returned {got!r}, expected '[Color(1, 2, 3)]'"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Points in a set

Point in main.py has a __repr__. Add __eq__ and __hash__ so that points with the same x and y are equal, a set keeps one of them, and an equal point finds a dict entry. For anything that is not a Point, __eq__ must return NotImplemented.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Without __eq__, two Point(1, 2) objects are different, because == falls back to identity.

  2. Hint 2

    In __eq__, check isinstance(other, Point) first and return NotImplemented when it is not.

  3. Hint 3

    Compare (self.x, self.y) == (other.x, other.y), and in __hash__ return hash((self.x, self.y)).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self) -> int:
        return hash((self.x, self.y))


if __name__ == "__main__":
    print({Point(1, 2), Point(1, 2)})
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

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

    # Add __eq__ (NotImplemented for other types) and a matching __hash__.


if __name__ == "__main__":
    print({Point(1, 2), Point(1, 2)})

test_main.py

from main import Point


def test_equal_points():
    """Two points with the same coordinates are equal"""
    assert Point(1, 2) == Point(1, 2), "Point(1, 2) == Point(1, 2) is False: compare x and y in __eq__"
    assert Point(1, 2) != Point(2, 1), "Point(1, 2) != Point(2, 1) is False"


def test_set_removes_duplicates():
    """A set keeps one of two equal points"""
    got = len({Point(1, 2), Point(1, 2), Point(3, 4)})
    assert got == 2, f"the set holds {got} points, expected 2"


def test_dict_key():
    """An equal point finds the dict entry"""
    names = {Point(0, 0): "origin"}
    got = names.get(Point(0, 0))
    assert got == "origin", f"names.get(Point(0, 0)) returned {got!r}, expected 'origin'"


def test_other_types():
    """__eq__ returns NotImplemented for anything that is not a Point"""
    got = Point(1, 2).__eq__((1, 2))
    assert got is NotImplemented, f"Point(1, 2).__eq__((1, 2)) returned {got!r}, expected NotImplemented"
    assert Point(1, 2) != (1, 2), "Point(1, 2) should not equal the tuple (1, 2)"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Defining __eq__ and forgetting __hash__

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

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


seen = {Point(1, 2)}

What Python prints

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

Why, and the fix

Overriding __eq__ sets __hash__ to None, so the objects cannot go into a set or be dict keys. Add def __hash__(self): return hash((self.x, self.y)), hashing the same fields __eq__ compares. If the objects are mutable, leave them unhashable on purpose.

__repr__ that returns something other than a str

class Order:
    def __init__(self, number):
        self.number = number

    def __repr__(self):
        return self.number


print(repr(Order(42)))

What Python prints

TypeError: __repr__ returned non-string (type int)

Why, and the fix

__repr__ and __str__ must return a str; Python checks the type and raises otherwise. Build the text instead: return f"Order({self.number!r})". The same goes for __hash__, which must return an int.

__eq__ that assumes the other object is the same type

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

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


print(Point(1) == "a")

What Python prints

AttributeError: 'str' object has no attribute 'x'

Why, and the fix

Any object can end up on the other side of ==, for example when a list is searched with in. Check the type first and return NotImplemented for foreign objects: if not isinstance(other, Point): return NotImplemented. Python then tries the other operand and finally answers False.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

repr for developers, str for users

repr(obj) calls __repr__, which should be unambiguous and, where possible, look like the expression that rebuilds the object: Point(1, 2). str(obj), print() and f"{obj}" call __str__, a readable form such as (1, 2). If a class defines only __repr__, str() falls back to it. The reverse is not true: containers show their items with repr, so print([p]) prints [Point(1, 2)] even when __str__ exists. In an f-string, !r asks for the repr.

Equality and the hash contract

By default == compares identity. Define __eq__ to compare by value, and return NotImplemented for types you do not handle, so Python can ask the other operand and finally fall back to identity. Sets and dicts find an object by its hash first and only then compare with ==. So the one rule for __hash__ is: objects that compare equal must have the same hash. The usual way is to hash a tuple of exactly the fields __eq__ compares: hash((self.x, self.y)).

Why __eq__ alone makes a class unhashable

The inherited hash is based on identity, which would break the rule as soon as two different objects compare equal. So a class that defines __eq__ without __hash__ gets __hash__ = None, and putting one in a set raises TypeError. @dataclass does the same unless frozen=True. A hash must also never change while the object sits in a set or dict: mutate a field the hash uses, and the object is lost in the wrong bucket. Hash only values that do not change.

Sources

Last reviewed September 29, 2026