From primitives to value objects
Passing money around as a bare float, or as a tuple of amount and currency, spreads its rules everywhere: every caller must remember the unit, check the currency, and format it the same way. This habit has a name, primitive obsession.
A small frozen dataclass gives the idea a name and one home for its rules. It checks itself when created, so an invalid value never exists, and its behaviour lives in methods next to the data.
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
cents: int
currency: str
def __add__(self, other):
if other.currency != self.currency:
message = "mixed currency"
raise ValueError(message)
total = self.cents + other.cents
return Money(total, self.currency)
print(Money(150, "EUR") + Money(75, "EUR"))Storing whole cents as an int avoids float rounding: 0.1 + 0.2 is not 0.3, but 10 + 20 is 30. Two values with the same fields compare equal, which is exactly how a value should behave.
Your task
Replace the tuples with a frozen dataclass Money(cents, currency). It raises ValueError unless the currency is three capital letters, + adds amounts of the same currency and raises ValueError for different ones, and str() gives text like 12.50 EUR.
The explanation opens here once every check passes.