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.

Easy Python 3.9+ Aug 9, 2026 OOP & classes 13 views 0 copies

Python code

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

stdout
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

  1. Use two-way composition: both classes hold references to each other for even more flexibility.
  2. 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

Run this sample

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

Open editor

More from OOP & classes

Related tutorials and quizzes for this topic.