A bank account is a good first class: it has data (the owner and the balance) and actions that change that data (deposit and withdraw). The class also checks every amount, so the balance can never go below zero.
The program
class BankAccount: def __init__(self, owner, opening_balance=0): self.owner = owner self._balance = 0 self.history = [] if opening_balance > 0: self.deposit(opening_balance) @property def balance(self): return self._balance def deposit(self, amount): if amount <= 0: raise ValueError("Deposit amount must be more than zero.") self._balance += amount self.history.append(("deposit", amount, self._balance)) def withdraw(self, amount): if amount <= 0: raise ValueError("Withdrawal amount must be more than zero.") if amount > self._balance: raise ValueError( f"Cannot withdraw {amount:.2f}: the balance is only {self._balance:.2f}." ) self._balance -= amount self.history.append(("withdraw", amount, self._balance)) def __repr__(self): return f"BankAccount(owner={self.owner!r}, balance={self._balance:.2f})" account = BankAccount("Ada", 100) account.deposit(50) account.withdraw(30) try: account.withdraw(500) except ValueError as error: print("Refused:", error) print(account) print(f"Balance: {account.balance:.2f}") print("History:") for kind, amount, balance_after in account.history: print(f" {kind:<9}{amount:>8.2f} balance {balance_after:.2f}")
Refused: Cannot withdraw 500.00: the balance is only 120.00. BankAccount(owner='Ada', balance=120.00) Balance: 120.00 History: deposit 100.00 balance 100.00 deposit 50.00 balance 150.00 withdraw 30.00 balance 120.00
How it works
self._balancestores the money. The leading underscore tells other programmers "do not change this directly".@propertymakesaccount.balancework like an attribute, but it is read-only.account.balance = 1000000raises anAttributeError, so the only way to change the balance is throughdeposit()andwithdraw(), which check the amount.withdraw()raises aValueErrorbefore it changes anything. The refused withdrawal of 500 leaves the balance and the history as they were.- The opening balance goes through
deposit(), so it is checked and recorded in the history like any other deposit. __repr__decides whatprint(account)shows.{self.owner!r}adds the quotes around the name.- Each history entry is a tuple: what happened, the amount, and the balance after it.
For real money, use whole cents (10050 for 100.50) or the decimal module, because floats cannot store most decimal fractions exactly. 0.1 + 0.2 is 0.30000000000000004.
The custom exception program replaces the ValueError for overdrafts with an InsufficientFundsError that also carries the balance and the amount. The property decorator program explains @property in more detail.