How to Create an Immutable Money Class in Python with dataclasses
Define a frozen dataclass Money that holds an amount and currency, enforces non-negative amounts, and supports safe addition across matching currencies.
Python code
30 linesfrom dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: float
currency: str = "USD"
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError("amount must be non-negative")
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("currencies must match")
return Money(self.amount + other.amount, self.currency)
def __str__(self) -> str:
return f"{self.amount:.2f} {self.currency}"
if __name__ == "__main__":
price = Money(19.99)
tax = Money(1.60)
total = price.add(tax)
print(total)
try:
bad = Money(-5)
except ValueError as e:
print(f"Error: {e}")
Output
21.59 USD
Error: amount must be non-negative
How it works
The @dataclass(frozen=True) decorator makes instances immutable, preventing accidental mutation after creation. __post_init__ runs validation right after initialization, raising a clear error for negative amounts. The add method checks currency compatibility and returns a new Money object, preserving immutability. The __str__ method formats the amount to two decimal places for clean display.
Common mistakes
- Forgetting to check for negative amounts in __post_init__
- Attempting to modify a frozen dataclass attribute, which raises FrozenInstanceError
- Allowing addition of different currencies without validation
Variations
- Use a NamedTuple instead of a dataclass for a simpler immutable representation
- Add a `to_dict` method or make Money a TypedDict for interfacing with JSON
Real-world use cases
- Representing monetary values in financial applications to ensure no accidental changes to calculated totals.
- Passing currency-specific prices through microservices without risk of mutation during processing.
- Using immutable money objects as keys in dictionaries or elements in sets, since they are hashable.
Sponsored
More from OOP & classes
- Add property getter setter validation in Python easy
- Binary Tree Inorder Traversal in Python easy
- Borg pattern shared state in Python medium
- Bridge Pattern in Python: Separate Abstraction from Implementation medium
- Composable Predicates with the &, |, ~ Operators in Python medium
- Composition over Inheritance: How to Build a Wallet Account in Python easy
Keep learning
Related tutorials and quizzes for this topic.