Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 11 of 11

Time to build a small banking system that uses everything from this module: a base class, two subclasses, protected state, validation, and dunder methods.

The base account

BankAccount protects its balance and routes every change through validated methods — the encapsulation from lesson 5.8.

class BankAccount:
    """A basic bank account with a protected balance."""

    account_count = 0

    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance
        self._transactions = []
        BankAccount.account_count += 1

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            return "Deposit must be positive"
        self._balance += amount
        self._transactions.append(("deposit", amount))
        return "OK"

    def withdraw(self, amount):
        if amount <= 0:
            return "Amount must be positive"
        if amount > self._balance:
            return "Insufficient funds"
        self._balance -= amount
        self._transactions.append(("withdraw", amount))
        return "OK"

    def __str__(self):
        return f"{self.owner}: ${self._balance:.2f}"

    def __repr__(self):
        return f"BankAccount({self.owner!r}, {self._balance})"

    def __len__(self):
        return len(self._transactions)

acct = BankAccount("Ada", 100)
acct.deposit(50)
acct.withdraw(30)
print(acct)
print(len(acct))

Note account_count is a class attribute updated with BankAccount.account_count += 1 — the lesson 5.4 rule. __len__ returns the transaction count, which is a natural meaning of "how big is this account's history."

Two subclasses

Both extend __init__ with super() and override withdraw() with their own rules.

class SavingsAccount(BankAccount):
    """Earns interest, but no overdraft."""

    def __init__(self, owner, balance=0, rate=0.02):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        interest = self._balance * self.rate
        self._balance += interest
        self._transactions.append(("interest", interest))
        return interest

    def __str__(self):
        return f"{super().__str__()} (savings)"


class CheckingAccount(BankAccount):
    """Allows going overdrawn up to a limit."""

    def __init__(self, owner, balance=0, overdraft=100):
        super().__init__(owner, balance)
        self.overdraft = overdraft

    def withdraw(self, amount):
        if amount <= 0:
            return "Amount must be positive"
        if amount > self._balance + self.overdraft:
            return "Exceeds overdraft limit"
        self._balance -= amount
        self._transactions.append(("withdraw", amount))
        return "OK"

    def __str__(self):
        return f"{super().__str__()} (checking)"

CheckingAccount.withdraw() fully replaces the parent's rule, because the logic genuinely differs. __str__ in both extends the parent with super().__str__() rather than repeating the formatting.

Polymorphism in action

The reporting code doesn't care what kind of account it has:

accounts = [
    SavingsAccount("Ada", 1000),
    CheckingAccount("Grace", 500),
    BankAccount("Alan", 250),
]

for account in accounts:
    print(account)

total = sum([a.balance for a in accounts])
print(f"Total: ${total:.2f}")

Adding a new account type means writing a new class — this loop never changes. That's lesson 5.9.

Try it yourself

Take it further

  • Add a transfer(self, other, amount) method that withdraws from one account and deposits into another — and refuses if the withdrawal fails.
  • Give BankAccount a history() method that returns a readable list of transactions.
  • Add __eq__ so two accounts are equal when they have the same owner and balance.
  • Add a Transaction class instead of tuples, with its own __repr__.

One real limitation: close the program and every account vanishes. Module 6 covers saving objects to a file so your data survives.

Module 5 complete

You can now design your own types: classes with constructors and methods, class versus instance data, inheritance with super(), encapsulated state, polymorphic behaviour, and dunder methods that make your objects feel native.

That's the toolkit for structuring a real codebase — instead of a pile of loose functions and dictionaries, you can model the actual things your program is about.

Next up: Module 6 — File Handling & Error Management, where your data finally starts surviving past the end of the program.

Questions about this lesson

Log in to ask a question.