A tiny class with the same shape as yours. The interesting line is the one that returns something other than True or False.
class Point:
def __init__(self, x):
self.x = x
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented # 'ask the other side'
return self.x == other.x
print(Point(1) == Point(1)) # -> True
print(Point(1) == 1) # -> False, after Python asked int too
print(Point(1) is Point(1)) # -> False, two different objects
- Without __eq__, equality means identity
By default two objects are equal only if they are the same object, so Point(1) == Point(1) would be False. For a value — a date, an amount of money, a coordinate — that is almost never what you want. Defining __eq__ is what turns an object into a value.
- NotImplemented is not False, and the difference matters
Returning NotImplemented tells Python 'I cannot answer this' and it then asks the *other* operand. Returning False answers on both their behalves and shuts the door — so a future class that knows how to compare itself with yours would never get the chance. Here both produce False, which is exactly why the habit has to be learned before it matters.
- __repr__ is for you, not for users
The default shows <Money object at 0x7f...>, which tells a debugger nothing. The convention is to print what you would type to recreate the value, so a list of them in a traceback is readable. __str__ is the separate, friendlier one for end users.
Four methods, and each maps to one line of the contract: how it is built, how it compares, how it adds, how it prints. Write __init__ first and get the two attribute cases passing before anything else.
__init__ stores the amount and currency.upper(). __eq__ takes (self, other) and must decide what to do when other is not a Money. __add__ returns a *new* Money rather than changing either side. __repr__ returns a string.
In __eq__, return NotImplemented when other is not a Money, then compare both attributes. In __add__, check the currencies match and raise ValueError if not, otherwise return Money(self.amount + other.amount, self.currency). In __repr__, use an f-string with {self.currency!r} so the quotes appear.
class Money: def __init__(self, amount, currency): self.amount = amount self.currency = currency.____() def __eq__(self, other): if not isinstance(other, Money): return ____ return self.amount == other.amount and ____ def __add__(self, other): if self.currency != other.currency: raise ____(...) return Money(____, self.currency) def __repr__(self): return f’Money({self.amount}, {self.currency!____})'
class Money:
"""An amount in minor units, with a currency code."""
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency.upper()
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(
f"cannot add {other.currency} to {self.currency}"
)
return Money(self.amount + other.amount, self.currency)
def __repr__(self):
return f"Money({self.amount}, {self.currency!r})"
Everything here could be done with two variables and a convention. The difference is that adding GBP to USD now raises instead of producing a plausible number, and no caller can forget the rule because they never see it. That is the test for whether a value class is worth writing: does it remove a way of being wrong, or just add a layer?
Mutating self would mean a + b silently changes a, which nobody expects from an arithmetic operator — the same instinct as leaving a caller’s list alone. Values should be immutable in practice even when Python does not enforce it; the next step is __hash__ and frozen dataclasses, which make it enforced.
A class that defines __eq__ without __hash__ becomes unhashable, so Money cannot go in a set or be a dict key. Python does that on purpose, because two objects that compare equal must hash equal, and it will not guess. Add __hash__ returning hash((self.amount, self.currency)) when you need it — or use a frozen dataclass, which writes both for you.
Reach for @dataclass(frozen=True) before hand-writing this: it generates __init__, __eq__, __repr__ and __hash__ correctly, and leaves you writing only __add__ and the currency normalisation. Writing them by hand once is how you learn what the decorator is doing; writing them by hand every time is how typos get into __eq__. And for real financial software, use a library — the py-moneyed and Decimal ecosystems already handle exchange rates, sub-unit precision that varies by currency, and the allocation rules that stop a split losing a cent.
- Add __hash__ so Money can be a dict key, then check that two equal Money objects land in the same bucket.
- Implement __sub__ and __mul__ by an integer, and decide deliberately whether Money times Money should exist at all.
- Rewrite the whole thing as a frozen dataclass and count how many lines survive — then work out which of them you would still have had to write yourself.