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.

Medium Python 3.9+ Aug 9, 2026 System design patterns 13 views 0 copies

Python code

43 lines
Python 3.9+
class 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

stdout
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

  1. Use `dataclass(frozen=True)` with a custom `__post_init__` to round the amount
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.