The Account Manager
An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- easy
- Seniority
- L4
- Asked in
- Notion
Problem
You're replaying a checking account's transaction log. Implement a BankAccount class that opens at initial_balance, grows on a deposit, and shrinks on a withdrawal but refuses any withdrawal larger than the current balance. An ['undo'] reverses the most recent deposit or withdrawal that is still standing, so several undos in a row peel back that many transactions, while a balance read or an undo with nothing left to reverse changes nothing. Drive it from bank_account_simulator(initial_balance, ops), which applies each op (['deposit', amt], ['withdraw', amt], ['undo'], or ['balance']) in order and returns the closing balance, or the string 'ValueError' the moment a withdrawal tries to pull more than the account holds.
Deposits, withdrawals, and a ledger you can walk back.
Example 1
Input:
initial_balance = 100, ops = [["deposit",50],["withdraw",30],["undo"],["balance"]]
Output:
150
Example 2
Input:
initial_balance = 0, ops = [["deposit",200],["withdraw",75],["deposit",25],["undo"],["undo"],["balance"]]
Output:
200
Worked solution and explanation
What this problem really is
Strip the banking costume off and this is reversible, ordered state hiding behind an account API. A plain balance += amount loop can add and subtract, but the moment an undo arrives it has nothing to reverse, and remembering just the single last change is worse: it serves one undo and then stalls, because a second undo in a row has to reach the transaction before it. So the real skill is keeping an ordered log of every change, a stack of signed deltas, so each undo pops the next one back. The interviewer is watching the boundary and the signal path: a withdrawal equal to the balance goes through, an overdraft halts the whole replay and comes back as the string 'ValueError' rather than clamping at zero or letting the exception escape, and consecutive undos peel back successive transactions while balance reads and spent undos leave no trace. Remember only the last change, or let one overdraft crash the driver, and the returned balance is a number the account never held.
Break down the requirements
Step 1: Constructor holds balance plus history
Store the balance as an instance attribute, and alongside it keep an ordered history (a stack) of the changes you apply. Without that full history a second undo has nothing to fall back to.
Step 2: Deposit adds and records
Plain addition, and record the change. There is no cap and no validation on the way up, but the deposit still has to leave a trace so it can be reversed later.
Step 3: Withdraw validates, then subtracts
Guard first, mutate second. Raise ValueError only when the amount is strictly greater than the current balance, so a withdrawal that drains the account to exactly zero still succeeds. Record the change once it goes through.
Step 4: Undo reverses the next transaction back
Pop the most recent recorded change and reverse it: add back a withdrawal, take back a deposit. Because you pop from an ordered log, calling undo again reaches the previous change, so consecutive undos walk backward through the transactions. If nothing has been recorded yet, undo is a no-op. Reads and prior undos leave no trace, so they are skipped automatically.
Step 5: Balance is a read-only property
Expose the balance as a read-only property so a 'balance' op is a pure read with no side effect and, crucially, leaves nothing on the history for undo to touch.
Step 6: Drive the class from one entrypoint
The grader calls one top-level entrypoint, so bank_account_simulator constructs a BankAccount, replays each op against it, and returns the final balance. Wrap the replay in try/except so the first overdraft stops the loop and surfaces as the string 'ValueError' instead of an uncaught exception.
The solution
class BankAccount:
def __init__(self, initial_balance):
self._balance = initial_balance
self._history = []
@property
def balance(self):
return self._balance
def deposit(self, amount):
self._balance += amount
self._history.append(amount)
def withdraw(self, amount):
if amount > self._balance:
raise ValueError('Insufficient funds')
self._balance -= amount
self._history.append(-amount)
def undo(self):
if not self._history:
return
delta = self._history.pop()
self._balance -= delta
def bank_account_simulator(initial_balance, ops):
account = BankAccount(initial_balance)
try:
for op in ops:
action = op[0]
if action == 'deposit':
account.deposit(op[1])
elif action == 'withdraw':
account.withdraw(op[1])
elif action == 'undo':
account.undo()
elif action == 'balance':
_ = account.balance
except ValueError:
return 'ValueError'
return account.balanceCommon follow-up questions
- How would you add a redo that re-applies the last undone transaction? (Tests extending the single history stack into an undo/redo pair.)
- How would you make a single account thread-safe under concurrent deposits and withdrawals? (Tests using a lock to guard both the balance and its history together.)
- What if a batch of operations had to apply atomically, rolling back entirely if any withdrawal failed? (Tests snapshot and rollback semantics layered on the existing history.)