thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · Python Project

Bank Account Simulator

Model bank accounts with deposits, withdrawals, and transfers — with proper rules and a transaction history. Learn modelling with classes.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want to model bank accounts: deposit, withdraw (but never below zero), transfer between accounts, and keep a transaction history. It teaches object-oriented modelling — representing real things as classes with data and rules that protect their own integrity.

Where this shows up: banking and fintech, e-commerce balances, game economies, inventory, any system where objects have state and rules that must never be broken. Classes that guard their own rules are the foundation of reliable software.

2 How to Think About It

Think about what an account must guarantee, before any code:

The plan — in plain English
1. An account has a balance and a history. → 2. Deposit adds money and records it. → 3. Withdraw removes money — but refuses if there are insufficient funds. → 4. Transfer is a withdraw from one plus a deposit to another. → The account protects its own rule: the balance can never go negative.

Deposit

Withdraw

Yes

No

Transfer

Account: balance + history

Action?

Add to balance, log it

Enough funds?

Subtract, log it

Reject

Withdraw from A, deposit to B

3 The Build — explained part by part

Here is the complete account model as a class. Each part is explained below.

Pythonbank.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.history = []

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError(&quot;Deposit must be positive&quot;)
        self.balance += amount
        self.history.append((&quot;deposit&quot;, amount))

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(&quot;Insufficient funds&quot;)
        self.balance -= amount
        self.history.append((&quot;withdraw&quot;, amount))

    def transfer(self, other, amount):
        # A transfer is a withdraw here plus a deposit there.
        self.withdraw(amount)
        other.deposit(amount)
        self.history.append((&quot;transfer_out&quot;, amount))

if __name__ == &quot;__main__&quot;:
    alice = Account(&quot;Alice&quot;, 100)
    bob = Account(&quot;Bob&quot;)
    alice.transfer(bob, 30)
    print(f&quot;Alice: {alice.balance}, Bob: {bob.balance}&quot;)
What each part does — in plain words
class Account: — a blueprint for an account. Each account made from it has its own balance and history.

__init__(self, owner, balance=0) — the constructor: sets up a new account with an owner, a starting balance, and an empty history list.

deposit — adds money, but first checks the amount is positive (you cannot deposit a negative). It logs every change to history.

withdraw — the crucial rule: if amount > self.balance: raise ValueError. The account refuses to go negative. This rule lives inside the class, so it can never be bypassed.

transfer — built from the other two: withdraw here, deposit there. Because it reuses withdraw, the insufficient-funds rule automatically applies to transfers too.

raise ValueError(...) — signal a broken rule by raising an error, so the caller must deal with it rather than silently corrupting the balance.
Common mistakes — and how to avoid them
✗ Letting the balance go negative — the whole point is it must never.
✓ Check funds in withdraw and raise an error if insufficient.
✗ Returning False on a bad withdrawal instead of raising — callers ignore return values and corrupt state.
✓ Raise an error so an illegal action cannot be silently ignored.
✗ Duplicating the funds check in transfer — then the rule lives in two places.
✓ Build transfer on top of withdraw so the rule is enforced once.

4 Test & Prove Each Part

We test that the account enforces its rules — especially that it refuses to overdraw.

A deposit increases the balance
A withdrawal within funds succeeds
Overdrawing is refused (raises an error)
A transfer moves money between accounts
Pythontest_bank.py
import pytest
from bank import Account


def test_deposit():
    &quot;&quot;&quot;Depositing increases the balance.&quot;&quot;&quot;
    a = Account(&quot;A&quot;)
    a.deposit(50)
    assert a.balance == 50


def test_withdraw():
    &quot;&quot;&quot;Withdrawing within funds works.&quot;&quot;&quot;
    a = Account(&quot;A&quot;, 100)
    a.withdraw(40)
    assert a.balance == 60


def test_overdraw_refused():
    &quot;&quot;&quot;Overdrawing raises an error and leaves the balance untouched.&quot;&quot;&quot;
    a = Account(&quot;A&quot;, 30)
    with pytest.raises(ValueError):
        a.withdraw(100)
    assert a.balance == 30


def test_transfer():
    &quot;&quot;&quot;Transfer moves money between accounts.&quot;&quot;&quot;
    a = Account(&quot;A&quot;, 100)
    b = Account(&quot;B&quot;)
    a.transfer(b, 30)
    assert a.balance == 70
    assert b.balance == 30

Run with pytest -v. The test_overdraw_refused test uses pytest.raises to prove the account rejects an illegal action and keeps its balance intact. Testing that bad actions are refused is as important as testing good ones work.

5 The Interface

depositaccount.deposit(amount)add money
withdrawaccount.withdraw(amount)remove money (refuses overdraft)
On insufficient funds
ValueError: Insufficient funds
transfera.transfer(b, amount)move money between accounts

6 Run It & Automate It

Run it locally
python3 bank.py
See a transfer in action. Try making it overdraw to watch the rule protect the balance.

Jenkins runs the tests automatically — each line explained below.

What you should see when it works
Terminala real run
Alice: 70, Bob: 30
If it breaks — how to fix it
🚨 The balance went negative.
Your withdraw check is wrong or missing. It must raise before subtracting when funds are short.
🚨 A failed transfer left money missing.
Withdraw should raise BEFORE depositing to the other account, so nothing moves on failure.
GroovyJenkinsfile
// Jenkinsfile — automated tests on every change.
pipeline {
    agent any
    stages {
        stage(&#x27;Get the code&#x27;) { steps { checkout scm } }       // download the code
        stage(&#x27;Install&#x27;) { steps { sh &#x27;python3 -m venv venv && . venv/bin/activate && pip install pytest&#x27; } }  // test tool
        stage(&#x27;Test&#x27;) { steps { sh &#x27;. venv/bin/activate && pytest -v&#x27; } }   // run every test
    }
    post {
        success { echo &#x27;All tests passed.&#x27; }
        failure { echo &#x27;A test failed — look above.&#x27; }
    }
}
🎯 Try this next — make it yours
  1. Interest. Add a method that applies interest to the balance. (Teaches: more behaviour.)
  2. Statement. Print the full transaction history nicely. (Teaches: using the history.)
  3. Account types. Make a SavingsAccount subclass with withdrawal limits. (Teaches: inheritance.)
What you learned
You learned object-oriented modelling: a class that bundles data (balance, history) with rules (no overdrafts) that protect its own integrity, signalling violations by raising errors. Self-protecting objects are the bedrock of reliable systems. Related: Object-Oriented Python, Error Handling.