Problem Set 14: Mini Bank System — Track Deposits, Withdrawals, Balances

When you start a new problem set, your first instinct might be to open your computer and begin typing code right away. While this can feel productive, it often leads to frustration when things don't work as expected. Instead, take a few minutes to slow down and plan.

Here are some helpful strategies:

  1. Understand the problem clearly
    • Read the instructions carefully — twice if needed.
    • Ask yourself: What exactly is being asked?
  2. Break the problem into smaller steps
    • Think about the smallest possible actions the computer will need to perform.
    • For example: If the task is to find the first recurring letter in a word, what steps must happen first?
  3. Try solving it on paper first
    • Write out your steps in plain language (pseudocode).
    • Test your steps with a simple example before touching the keyboard.
  4. Translate your steps into code
    • Start small — write only a few lines at a time and test often.
    • Don't worry about perfection at first; get a working version, then improve it.
  5. Check your solution
    • Run it with different examples, including edge cases.
    • Ask: Does this solve the problem in all situations?

  • Read the question carefully (twice).
  • Break the task into the smallest steps.
  • Sketch or write pseudocode before coding.
  • Start small — test as you go.
  • Check your solution with different cases.

Overview

Build a simple command-line “mini bank” that supports multiple accounts. Users can create an account, deposit money, withdraw money (with basic rules), and check balances. You’ll add input validation and a small test suite that checks both correct (good) and incorrect (bad) behavior.

Learning Goals

  • Use dictionaries/lists to model state.
  • Write small, single-purpose functions with clear inputs/outputs.
  • Validate and sanitize user input.
  • Handle error conditions without crashing (return error messages / codes).
  • Write automated tests that assert both expected success and expected failure paths.

Functional Requirements

Implement these functions (names suggested; you may adapt if you keep behavior the same):

  1. create_account(accounts, name)
    • Creates a new account with balance 0.0 and empty history.
    • Error if the name already exists or is empty/whitespace.
  2. deposit(accounts, name, amount)
    • Adds amount to balance and records a transaction.
    • Errors if account doesn’t exist, amount <= 0, or amount is not numeric.
  3. withdraw(accounts, name, amount)
    • Subtracts amount from balance and records a transaction.
    • Errors if account doesn’t exist, amount <= 0, or insufficient funds.
  4. get_balance(accounts, name)
    • Returns current balance.
    • Errors if account doesn’t exist.
  5. get_history(accounts, name, n=None)
    • Returns last n transactions (or all if n is None).

Data Model (suggestion)

accounts = {
    "Alice": {
        "balance": 125.50,
        "history": [
            {"type": "deposit", "amount": 200.00},
            {"type": "withdraw", "amount": 74.50},
        ]
    },
    # ...
}

Validation Rules

  • Account names: non-empty after strip().
  • Amounts: numeric, strictly greater than 0.
  • Withdrawals: cannot create negative balances (no overdraft).
  • All functions should return structured results instead of printing:
    • On success: {"ok": True, "data": ...}
    • On error: {"ok": False, "error": "message"}

CLI (Optional but encouraged)

A simple loop:

> create Alice
> deposit Alice 100
> withdraw Alice 40
> balance Alice
> history Alice 5
> quit

Parse commands, call your functions, and print results.


Milestones

  1. Data model + create_account + get_balance
  2. deposit + withdraw with validation
  3. Transaction history
  4. CLI wrapper (optional)
  5. Tests (good + bad)

Hints

  • Keep calculations to two decimal places. Beginners may use round(x, 2); strong students can use Decimal.
  • Centralize validation (e.g., a helper parse_amount(x)).
  • Never duplicate account names.

Starter Skeleton (students fill in bodies)

# bank.py
from typing import Dict, Any, Optional, List

def _err(msg: str) -> Dict[str, Any]:
    return {"ok": False, "error": msg}

def _ok(data: Any = None) -> Dict[str, Any]:
    return {"ok": True, "data": data}

def create_account(accounts: Dict[str, Any], name: str) -> Dict[str, Any]:
    name = (name or "").strip()
    if not name:
        return _err("Account name cannot be empty.")
    if name in accounts:
        return _err("Account already exists.")
    accounts[name] = {"balance": 0.0, "history": []}
    return _ok({"name": name, "balance": 0.0})

def deposit(accounts: Dict[str, Any], name: str, amount: Any) -> Dict[str, Any]:
    if name not in accounts:
        return _err("Account does not exist.")
    try:
        amount = float(amount)
    except (TypeError, ValueError):
        return _err("Amount must be a number.")
    if amount <= 0:
        return _err("Amount must be greater than 0.")
    accounts[name]["balance"] = round(accounts[name]["balance"] + amount, 2)
    accounts[name]["history"].append({"type": "deposit", "amount": round(amount, 2)})
    return _ok({"name": name, "balance": accounts[name]["balance"]})

def withdraw(accounts: Dict[str, Any], name: str, amount: Any) -> Dict[str, Any]:
    if name not in accounts:
        return _err("Account does not exist.")
    try:
        amount = float(amount)
    except (TypeError, ValueError):
        return _err("Amount must be a number.")
    if amount <= 0:
        return _err("Amount must be greater than 0.")
    if accounts[name]["balance"] < amount:
        return _err("Insufficient funds.")
    accounts[name]["balance"] = round(accounts[name]["balance"] - amount, 2)
    accounts[name]["history"].append({"type": "withdraw", "amount": round(amount, 2)})
    return _ok({"name": name, "balance": accounts[name]["balance"]})

def get_balance(accounts: Dict[str, Any], name: str) -> Dict[str, Any]:
    if name not in accounts:
        return _err("Account does not exist.")
    return _ok({"name": name, "balance": accounts[name]["balance"]})

def get_history(accounts: Dict[str, Any], name: str, n: Optional[int] = None) -> Dict[str, Any]:
    if name not in accounts:
        return _err("Account does not exist.")
    hist = accounts[name]["history"]
    if n is not None:
        if not isinstance(n, int) or n <= 0:
            return _err("n must be a positive integer.")
        hist = hist[-n:]
    return _ok(list(hist))

Manual Black-Box Tests (Good & Bad)

Good (expected to succeed)

  1. Create account
    • Input: create_account({}, "Alice")
    • Expect: ok=True, balance 0.0
  2. Deposit positive number
    • Start: Alice: 0.0
    • Input: deposit(accounts, "Alice", 100)
    • Expect: ok=True, balance 100.0
  3. Withdraw with sufficient funds
    • Start: Alice: 100.0
    • Input: withdraw(accounts, "Alice", 40.25)
    • Expect: ok=True, balance 59.75 (rounded)
  4. Get balance
    • Input: get_balance(accounts, "Alice")
    • Expect: ok=True, data 59.75
  5. History last 2
    • After the above deposit/withdraw
    • Input: get_history(accounts, "Alice", 2)
    • Expect: [{"type":"deposit","amount":100.0},{"type":"withdraw","amount":40.25}]

Bad (expected to fail safely)

  1. Duplicate account
    • Input: create “Alice” twice
    • Expect: second call ok=False, error “Account already exists.”
  2. Empty account name
    • Input: create_account(accounts, " ")
    • Expect: ok=False, “Account name cannot be empty.”
  3. Deposit non-numeric
    • Input: deposit(accounts, "Alice", "ten")
    • Expect: ok=False, “Amount must be a number.”
  4. Deposit zero/negative
    • Input: deposit(accounts, "Alice", 0) or -5
    • Expect: ok=False, “Amount must be greater than 0.”
  5. Withdraw non-existent account
    • Input: withdraw(accounts, "Bob", 10)
    • Expect: ok=False, “Account does not exist.”
  6. Withdraw insufficient funds
    • Start: Alice: 5.00
    • Input: withdraw(accounts, "Alice", 10)
    • Expect: ok=False, “Insufficient funds.”
  7. History invalid n
    • Input: get_history(accounts, "Alice", n=0) or n="two"
    • Expect: ok=False, “n must be a positive integer.”

Automated Tests pytest

Save this as test_bank.py alongside bank.py.

# test_bank.py
import bank

def make_accounts():
    return {}

def test_create_and_initial_balance():
    accounts = make_accounts()
    r = bank.create_account(accounts, "Alice")
    assert r["ok"] is True
    assert accounts["Alice"]["balance"] == 0.0

def test_create_duplicate_account_fails():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    r = bank.create_account(accounts, "Alice")
    assert r["ok"] is False
    assert "exists" in r["error"].lower()

def test_deposit_happy_path():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    r = bank.deposit(accounts, "Alice", 100)
    assert r["ok"] is True
    assert r["data"]["balance"] == 100.0

def test_deposit_rejects_non_numeric_and_non_positive():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    r1 = bank.deposit(accounts, "Alice", "ten")
    r2 = bank.deposit(accounts, "Alice", 0)
    r3 = bank.deposit(accounts, "Alice", -1)
    assert r1["ok"] is False and "number" in r1["error"].lower()
    assert r2["ok"] is False and "greater than 0" in r2["error"].lower()
    assert r3["ok"] is False and "greater than 0" in r3["error"].lower()

def test_withdraw_happy_path_and_rounding():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    bank.deposit(accounts, "Alice", 100)
    r = bank.withdraw(accounts, "Alice", 40.255)  # tests rounding
    assert r["ok"] is True
    assert r["data"]["balance"] == 59.75

def test_withdraw_insufficient_and_missing_account():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    bank.deposit(accounts, "Alice", 5)
    r1 = bank.withdraw(accounts, "Alice", 10)
    r2 = bank.withdraw(accounts, "Bob", 1)
    assert r1["ok"] is False and "insufficient" in r1["error"].lower()
    assert r2["ok"] is False and "does not exist" in r2["error"].lower()

def test_get_balance_and_history():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    bank.deposit(accounts, "Alice", 100)
    bank.withdraw(accounts, "Alice", 25)
    b = bank.get_balance(accounts, "Alice")
    h = bank.get_history(accounts, "Alice", 2)
    assert b["ok"] is True and b["data"]["balance"] == 75.0
    assert h["ok"] is True and h["data"] == [
        {"type": "deposit", "amount": 100.0},
        {"type": "withdraw", "amount": 25.0},
    ]

def test_history_bad_n():
    accounts = make_accounts()
    bank.create_account(accounts, "Alice")
    r1 = bank.get_history(accounts, "Alice", 0)
    r2 = bank.get_history(accounts, "Alice", "two")
    assert r1["ok"] is False and "positive integer" in r1["error"].lower()
    assert r2["ok"] is False and "positive integer" in r2["error"].lower()

Run with:

pytest -q

Extensions (Optional)

  • Use decimal.Decimal for precise currency.
  • Persist accounts to a JSON file (save_accounts, load_accounts).
  • Add transfers: transfer(accounts, from_name, to_name, amount) with atomic checks.
  • Add simple authentication (PIN per account) for the CLI.