Warm-up · Activity 1 of 7
// 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
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
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])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))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__)Practice · Activity 5 of 7
The class defines both __repr__ and __str__. Match each expression to the method that produces its text.
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)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.pyOutput
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
Hint 1
Both methods take only self and must return a str, not print it.
Hint 2
The format spec 02x writes an int as two lower-case hex digits: f"{10:02x}" is 0a.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 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
Hint 1
Without __eq__, two Point(1, 2) objects are different, because == falls back to identity.
Hint 2
In __eq__, check isinstance(other, Point) first and return NotImplemented when it is not.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon 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.