- 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):
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.
deposit(accounts, name, amount)- Adds
amountto balance and records a transaction. - Errors if account doesn’t exist,
amount <= 0, oramountis not numeric.
- Adds
withdraw(accounts, name, amount)- Subtracts
amountfrom balance and records a transaction. - Errors if account doesn’t exist,
amount <= 0, or insufficient funds.
- Subtracts
get_balance(accounts, name)- Returns current balance.
- Errors if account doesn’t exist.
get_history(accounts, name, n=None)- Returns last
ntransactions (or all ifnisNone).
- Returns last
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"}
- On success:
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
- Data model +
create_account+get_balance deposit+withdrawwith validation- Transaction history
- CLI wrapper (optional)
- Tests (good + bad)
Hints
- Keep calculations to two decimal places. Beginners may use
round(x, 2); strong students can useDecimal. - 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)
- Create account
- Input:
create_account({}, "Alice") - Expect:
ok=True, balance0.0
- Input:
- Deposit positive number
- Start:
Alice: 0.0 - Input:
deposit(accounts, "Alice", 100) - Expect:
ok=True, balance100.0
- Start:
- Withdraw with sufficient funds
- Start:
Alice: 100.0 - Input:
withdraw(accounts, "Alice", 40.25) - Expect:
ok=True, balance59.75(rounded)
- Start:
- Get balance
- Input:
get_balance(accounts, "Alice") - Expect:
ok=True, data59.75
- Input:
- 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)
- Duplicate account
- Input: create “Alice” twice
- Expect: second call
ok=False, error “Account already exists.”
- Empty account name
- Input:
create_account(accounts, " ") - Expect:
ok=False, “Account name cannot be empty.”
- Input:
- Deposit non-numeric
- Input:
deposit(accounts, "Alice", "ten") - Expect:
ok=False, “Amount must be a number.”
- Input:
- Deposit zero/negative
- Input:
deposit(accounts, "Alice", 0)or-5 - Expect:
ok=False, “Amount must be greater than 0.”
- Input:
- Withdraw non-existent account
- Input:
withdraw(accounts, "Bob", 10) - Expect:
ok=False, “Account does not exist.”
- Input:
- Withdraw insufficient funds
- Start:
Alice: 5.00 - Input:
withdraw(accounts, "Alice", 10) - Expect:
ok=False, “Insufficient funds.”
- Start:
- History invalid n
- Input:
get_history(accounts, "Alice", n=0)orn="two" - Expect:
ok=False, “n must be a positive integer.”
- Input:
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.Decimalfor 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.