-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
137: Practice Exercise: Modeling a Bank Account System with Classes
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
SavingsAccountthat inherits from theBankAccountclass we built in the lesson. - Add a new attribute to
SavingsAccountcalledinterest_rate(a float, e.g.,0.02for 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 thehistorylist (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 anotherBankAccountinstance. If the withdrawal fails (insufficient funds), the transfer should not happen.
Test your code by:
- Creating a
SavingsAccountwith $1,000 and an interest rate of 5%. - Applying interest and verifying the balance is now $1,050.
- Creating a second
BankAccountand transferring $200 from the savings account to the second account. - Printing the transaction history of both accounts to ensure the logs are accurate.
There are no comments for now.