Composition over Inheritance: How to Build a Wallet Account in Python
Demonstrates composition by wrapping a WalletAccount class in an AuditedWallet decorator-like class to add behavior without changing the original class.
Python code
60 linesclass WalletAccount:
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
def transfer(sender, receiver, amount):
sender.withdraw(amount)
receiver.deposit(amount)
return f"Transferred ${amount:.2f} from {sender.owner} to {receiver.owner}"
class AuditedWallet:
def __init__(self, wallet):
self._wallet = wallet
self.transactions = []
def deposit(self, amount):
result = self._wallet.deposit(amount)
self.transactions.append(f"Deposited ${amount:.2f}")
return result
def withdraw(self, amount):
result = self._wallet.withdraw(amount)
self.transactions.append(f"Withdrew ${amount:.2f}")
return result
@property
def balance(self):
return self._wallet.balance
@property
def owner(self):
return self._wallet.owner
if __name__ == "__main__":
alice = AuditedWallet(WalletAccount("Alice", 100.0))
bob = WalletAccount("Bob", 50.0)
alice.withdraw(30.0)
transfer(alice, bob, 20.0)
print(f"Alice balance: ${alice.balance:.2f}")
print(f"Bob balance: ${bob.balance:.2f}")
print(f"Audit log for Alice: {alice.transactions}")
Output
Alice balance: $50.00
Bob balance: $70.00
Audit log for Alice: ['Withdrew $30.00', 'Withdrew $20.00']
How it works
Composition means building new functionality by combining and wrapping objects rather than inheriting from them. Here, AuditedWallet holds a reference to a WalletAccount instance and delegates method calls to it, adding audit logging on top. This keeps the original WalletAccount simple and single-purpose, and the wrapper can be applied to any object with a compatible interface. Properties balance and owner expose the underlying wallet's attributes while keeping the interface consistent. This pattern avoids brittle inheritance hierarchies and allows mixing behaviors dynamically.
Common mistakes
- Forgetting to delegate all public methods, causing AttributeError for missing methods
- Relying on inheritance for cross-cutting concerns like logging, leading to tangled hierarchies
- Not using @property for delegated attributes, breaking encapsulation when state changes
- Assuming the wrapper fully mimics the inner object, missing that it's a separate class
Variations
- Use two-way composition: both classes hold references to each other for even more flexibility.
- Implement a `Proxy` or `Decorator` pattern from the standard library `abc` to enforce the interface.
Real-world use cases
- Adding audit trails to financial services without modifying core transaction code.
- Wrapping legacy library calls with logging/metrics in a dependency-injection-friendly way.
- Composing retry or validation logic on top of an existing API client without subclassing.
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
- Compute Derived Fields with @dataclass __post_init__ in Python easy
Keep learning
Related tutorials and quizzes for this topic.