How to Build an Immutable Money Value Object in Python
Implement an immutable Money class with rounded decimal amounts, currency, safe equality, and hashing for use as a value object.
Python code
43 linesclass Money:
def __init__(self, amount: float, currency: str):
object.__setattr__(self, "_amount", round(amount, 2))
object.__setattr__(self, "_currency", currency)
def __setattr__(self, name, value):
raise AttributeError(f"Money is immutable: cannot set '{name}'")
def __delattr__(self, name):
raise AttributeError("Money is immutable: cannot delete attributes")
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self._amount == other._amount and self._currency == other._currency
def __hash__(self):
return hash((self._amount, self._currency))
def __repr__(self):
return f"Money({self._amount}, '{self._currency}')"
def amount(self):
return self._amount
def currency(self):
return self._currency
if __name__ == "__main__":
m1 = Money(19.999, "USD")
m2 = Money(20.00, "USD")
m3 = Money(19.99, "EUR")
print(m1)
print(m1 == m2, m1 == m3)
print(f"Amount: {m1.amount()}, Currency: {m1.currency()}")
print("Unique set:", {m1, m2, m3})
try:
m1.amount = 100
except AttributeError as e:
print(f"Immutable check: {e}")
Output
Money(20.0, 'USD')
True False
Amount: 20.0, Currency: USD
Unique set: {Money(20.0, 'USD'), Money(19.99, 'EUR')}
Immutable check: Money is immutable: cannot set 'amount'
How it works
The __init__ uses object.__setattr__ to bypass the blocked __setattr__, which enforces immutability after construction. The __eq__ and __hash__ methods make the class behave like a true value object, enabling use in sets and as dictionary keys. Rounding the amount to two decimals prevents floating-point drift in financial calculations. The __repr__ provides a readable string for debugging and logging. The class blocks attribute mutation and deletion, ensuring state integrity throughout the object's lifetime.
Common mistakes
- Overriding `__setattr__` without using `object.__setattr__` in `__init__`, causing an infinite recursion
- Forgetting to implement `__hash__` when `__eq__` is overridden, breaking set/dict usage
- Using `float` for money without rounding, leading to precision errors in comparisons
- Comparing a Money instance with a non-Money object and returning `False` instead of `NotImplemented`
Variations
- Use `dataclass(frozen=True)` with a custom `__post_init__` to round the amount
- Add arithmetic operators like `__add__` and `__sub__` for money operations
Real-world use cases
- Modeling currency-safe prices in e-commerce carts where values must be compared in sets. (89 chars)
- Passing immutable payment amounts between microservices to prevent accidental mutation during request processing. (120 chars)
- Storing order line items in a cache keyed by money — hashing immutable values ensures stable lookups. (104 chars)
Sponsored
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Domain Driven Design Aggregate Root Example in Python medium
- Facade Pattern in Python with Mock Simplification medium
Keep learning
Related tutorials and quizzes for this topic.