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

Stateful class with a history stack, driven by a top-level function
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.balance

Common follow-up questions