Lab 05 — Bank Account System
Hands-on Lab 60 min Intermediate
Objectives
- Design a class hierarchy with abstract base class
- Implement properties with validation
- Override dunder methods (
__str__,__repr__,__eq__,__lt__) - Use
super()properly in child classes - Apply
@dataclassfor simple records
Step 1 — Create the Project
mkdir ~/python-course/lab-05 && cd ~/python-course/lab-05
Create bank.py:
#!/usr/bin/env python3
"""
Lab 05 — Bank Account System (OOP)
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from functools import total_ordering
@dataclass
class Transaction:
"""Represents a single account transaction."""
amount: float
type: str
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
description: str = ""
def __str__(self):
sign = "+" if self.amount >= 0 else ""
return f"[{self.timestamp}] {self.type:<10} {sign}{self.amount:>10.2f} {self.description}"
@total_ordering
class Account(ABC):
"""Abstract base class for all account types."""
_next_id = 1000
def __init__(self, owner: str, initial_balance: float = 0.0):
self._id = Account._next_id
Account._next_id += 1
self._owner = owner
self._balance = 0.0
self._transactions: list[Transaction] = []
if initial_balance > 0:
self._deposit_raw(initial_balance, "Initial deposit")
def _deposit_raw(self, amount: float, description=""):
self._balance += amount
self._transactions.append(
Transaction(amount, "DEPOSIT", description=description)
)
def _withdraw_raw(self, amount: float, description=""):
self._balance -= amount
self._transactions.append(
Transaction(-amount, "WITHDRAW", description=description)
)
@property
def balance(self) -> float:
return self._balance
@property
def owner(self) -> str:
return self._owner
@property
def account_id(self) -> int:
return self._id
@abstractmethod
def deposit(self, amount: float, description: str = "") -> float:
...
@abstractmethod
def withdraw(self, amount: float, description: str = "") -> float:
...
@abstractmethod
def account_type(self) -> str:
...
def transfer_to(self, target: "Account", amount: float) -> bool:
"""Transfer money to another account."""
try:
self.withdraw(amount, f"Transfer to #{target.account_id}")
target.deposit(amount, f"Transfer from #{self.account_id}")
return True
except ValueError:
return False
def statement(self):
"""Print account statement."""
print(f"\n{'='*60}")
print(f" {self.account_type()} — {self._owner} (#{self._id})")
print(f"{'='*60}")
for tx in self._transactions:
print(f" {tx}")
print(f"{'─'*60}")
print(f" {'Current Balance':>30}: ${self._balance:>12,.2f}")
print()
def __repr__(self):
return f"{self.__class__.__name__}(id={self._id}, owner='{self._owner}', balance={self._balance:.2f})"
def __str__(self):
return f"{self.account_type()} #{self._id} ({self._owner}) — ${self._balance:,.2f}"
def __eq__(self, other):
if not isinstance(other, Account):
return NotImplemented
return self._id == other._id
def __lt__(self, other):
if not isinstance(other, Account):
return NotImplemented
return self._balance < other._balance
class CheckingAccount(Account):
"""Checking account with overdraft protection."""
def __init__(self, owner, initial_balance=0.0, overdraft_limit=500.0):
super().__init__(owner, initial_balance)
self._overdraft_limit = overdraft_limit
def account_type(self):
return "Checking Account"
def deposit(self, amount, description=""):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._deposit_raw(amount, description)
return self._balance
def withdraw(self, amount, description=""):
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if self._balance - amount < -self._overdraft_limit:
raise ValueError(f"Exceeds overdraft limit of ${self._overdraft_limit:.2f}")
self._withdraw_raw(amount, description)
return self._balance
class SavingsAccount(Account):
"""Savings account with interest and withdrawal limits."""
def __init__(self, owner, initial_balance=0.0, interest_rate=0.02):
super().__init__(owner, initial_balance)
self._interest_rate = interest_rate
self._withdrawals_this_month = 0
self._max_monthly_withdrawals = 6
def account_type(self):
return "Savings Account"
def deposit(self, amount, description=""):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._deposit_raw(amount, description)
return self._balance
def withdraw(self, amount, description=""):
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
if self._withdrawals_this_month >= self._max_monthly_withdrawals:
raise ValueError("Monthly withdrawal limit reached")
self._withdraw_raw(amount, description)
self._withdrawals_this_month += 1
return self._balance
def apply_interest(self):
interest = round(self._balance * self._interest_rate, 2)
self._deposit_raw(interest, f"Interest ({self._interest_rate:.1%})")
return interest
def demo():
print("🏦 Python National Bank — Lab 05\n")
checking = CheckingAccount("Alice Martin", initial_balance=2000, overdraft_limit=500)
savings = SavingsAccount("Alice Martin", initial_balance=5000, interest_rate=0.03)
checking.deposit(1500, "Salary")
checking.withdraw(800, "Rent")
checking.withdraw(150, "Groceries")
savings.deposit(500, "Monthly savings")
interest = savings.apply_interest()
print(f" Interest earned: ${interest:.2f}")
success = checking.transfer_to(savings, 300)
print(f" Transfer: {'✅' if success else '❌'}")
checking.statement()
savings.statement()
accounts = [checking, savings]
print(f"\n Accounts sorted by balance:")
for a in sorted(accounts):
print(f" {a}")
print(f"\n checking == savings: {checking == savings}")
print(f" checking < savings : {checking < savings}")
if __name__ == "__main__":
demo()
Step 2 — Run
python3 bank.py
Verification ✅
🏦 Python National Bank — Lab 05
Interest earned: $165.00
Transfer: ✅
Checking Account — Alice Martin (#1000)
============================================================
[2026-03-29 10:00:00] DEPOSIT + 2000.00 Initial deposit
[2026-03-29 10:00:00] DEPOSIT + 1500.00 Salary
...
Summary
- Abstract base class
Accountwith@abstractmethodforces subclasses to implementdeposit/withdraw @dataclassfor the simpleTransactionrecord@propertyfor controlled access to_balance,_owner@total_orderinggenerates all comparison operators from__eq__+__lt__super().__init__(...)initializes parent state in all subclass constructorstransfer_to()demonstrates polymorphism — works with anyAccountsubclass