Skip to Content
Course content

137: Practice Exercise: Modeling a Bank Account System with Classes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've talked about the theory of classes, but the best way to actually "get" object-oriented programming is to try and model something from the real world. Let's build a bank account system. Now, I could just give you the finished code, but it's more helpful if we build it the way I do when I'm prototyping—starting simple, breaking it, and then fixing it.

Starting with a Bare-Bones Account

If I'm thinking about a bank account, the most basic thing it needs is a balance and a way to change that balance. I'll start with a simple class that handles deposits and withdrawals.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount}. New balance: ${self.balance}")

    def withdraw(self, amount):
        self.balance -= amount
        print(f"Withdrew ${amount}. New balance: ${self.balance}")

# Let's test it out
my_account = BankAccount("Alice", 100)
my_account.withdraw(150)

I ran this, and it worked—technically. But look at the result: Alice now has -$50. Unless we're building a high-interest overdraft facility, this is a bug. In the real world, the system should stop you from taking money you don't have.

Wait, I Can't Just Go Into Debt

I need to add some logic to the withdraw method to prevent a negative balance. I'll add a conditional check, but I also want to make sure the user knows why the transaction failed.

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds! Transaction cancelled.")
            return False # Return False so the calling code knows it failed
        
        self.balance -= amount
        print(f"Withdrew ${amount}. New balance: ${self.balance}")
        return True

That's better. Now, if I try to withdraw $150 from a $100 account, it catches the error. But as I'm testing this, I realize a problem: if I look at the account, I only see the current balance. If a customer calls the bank asking why their balance is $50, I have no way of seeing the history. I've got the state, but I've lost the story.

The Need for a Paper Trail

To fix this, I'll add a list to keep track of every transaction. I'll initialize it as an empty list in the __init__ method and append a description every time money moves.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.history = [] # Our ledger

    def deposit(self, amount):
        self.balance += amount
        self.history.append(f"Deposit: +${amount}")
        print(f"Deposited ${amount}. New balance: ${self.balance}")

    def withdraw(self, amount):
        if amount > self.balance:
            self.history.append(f"Failed Withdrawal: -${amount} (Insufficient Funds)")
            print("Insufficient funds!")
            return False
        
        self.balance -= amount
        self.history.append(f"Withdrawal: -${amount}")
        print(f"Withdrew ${amount}. New balance: ${self.balance}")
        return True

Now I can just print my_account.history and see exactly what happened. But here is where I run into a classic Python problem. If I'm a developer working on a different part of the app, I might be tempted to do something like this: my_account.balance = 1000000. I've just bypassed all the logic and "cheated" the system.

Stopping the 'Cheating' Developer

In some languages, I'd make the balance variable private. Python doesn't have strict private variables, but we have a convention: prefixing a variable with an underscore (_balance) tells other developers, "Hey, don't touch this directly." To allow them to see the balance without changing it, I'll use a property.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance # Underscore indicates 'protected'
        self.history = []

    @property
    def balance(self):
        """This allows us to read the balance, but not overwrite it directly."""
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            print("Must deposit a positive amount.")
            return
        self._balance += amount
        self.history.append(f"Deposit: +${amount}")

    def withdraw(self, amount):
        if amount > self._balance:
            print("Insufficient funds!")
            return False
        self._balance -= amount
        self.history.append(f"Withdrawal: -${amount}")
        return True

By using @property, I've created a read-only attribute. If someone tries to run my_account.balance = 1000000 now, Python will throw an AttributeError. I've forced the interaction to happen through the deposit and withdraw methods, which is exactly how a real-world system should behave.




📋 Practical Task

Exercise: Implementing an Interest-Bearing Savings Account

Now it's your turn to extend this logic. You need to create a specialized version of the bank account that handles interest.

Your Task:

  • Create a class called SavingsAccount that inherits from the BankAccount class we built in the lesson.
  • Add a new attribute to SavingsAccount called interest_rate (a float, e.g., 0.02 for 2%).
  • Implement a method called apply_interest(). This method should calculate the interest based on the current balance, add it to the balance, and record the transaction in the history list (e.g., "Interest Added: +$2.00").
  • Implement a method called transfer(self, target_account, amount). This method should withdraw money from the current account and deposit it into another BankAccount instance. If the withdrawal fails (insufficient funds), the transfer should not happen.

Test your code by:

  1. Creating a SavingsAccount with $1,000 and an interest rate of 5%.
  2. Applying interest and verifying the balance is now $1,050.
  3. Creating a second BankAccount and transferring $200 from the savings account to the second account.
  4. Printing the transaction history of both accounts to ensure the logs are accurate.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.