Encapsulation
Our account works, and it has a serious hole: anyone can do this.
That isn't a bug in the code — it's a bug in the design. The rule "a balance can't go negative" exists only in the heads of the people writing the callers.
Encapsulation is the fix: put every rule about the data inside the object that owns it, so breaking the rule stops being possible rather than merely discouraged.
Methods as the only way in
The first move is simple — no caller touches balance directly; they ask.
The guard runs before the balance is touched. Get that order wrong and you create exactly the state you were trying to prevent:
This should refuse the withdrawal and leave the balance at 100. It prints -400. Run it, then fix the order.
The underscore convention
Python has no private keyword. What it has is a convention that everyone
follows: a leading underscore means this is internal, don't reach for it.
@property makes balance read like a plain attribute while running a method
underneath. Because there's no setter, assigning to it is refused outright:
class Account:
def __init__(self, balance=0):
self._balance = balance
@property
def balance(self):
return self._balance
a = Account(50)
a.balance = 999Nothing stops a determined caller reaching for _balance directly. That's a
deliberate choice in Python's design: it trusts you, and makes the intent
obvious rather than enforcing it with the type system.
Validate at the door
The strongest version is refusing to build a bad object at all:
Now there is no moment, however brief, in which an Account exists holding
nonsense. Compare that with checking afterwards, where the bad object is real for
a while and something might read it.
Step through a refusal and watch the balance simply not move:
🎯 Your turn
Give Account a withdraw(self, amount) that refuses to overdraw — take
nothing and return False when there aren't enough funds, otherwise deduct and
return True. Then write safe_withdraw(start, amounts) which tries each
withdrawal in turn and returns the final balance:
safe_withdraw(100, [30, 200, 20])→50(the 200 is refused)safe_withdraw(50, [50])→0(exact is allowed)
Hint — guard first, deduct second. amount > self.balance is the test.
Then press ▶ Run and hit ✓ Check. ✅
