Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Objects  ›  Lesson

Encapsulation

Encapsulation 16 minMaking the wrong state impossible, not merely discouraged
You're building a piece ofA bank account that can't go wrong
This piece — Account.withdraw(): Makes overdrawing impossible rather than merely discouraged.
Scenario A customer with £100 tries to take out £200. The account must decline rather than quietly going negative.
Your task
Give Account a withdraw(self, amount) that refuses to overdraw — it takes nothing out and returns False when there are insufficient funds, otherwise deducts and returns True. Then write safe_withdraw(start, amounts) which tries each withdrawal in turn and returns the final balance. Example: safe_withdraw(100, [30, 200, 20]) → 50, because the 200 is refused.

Encapsulation

Our account works, and it has a serious hole: anyone can do this.

python

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.

python

The guard runs before the balance is touched. Get that order wrong and you create exactly the state you were trying to prevent:

broken — fix it

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.

python

@property makes balance read like a plain attribute while running a method underneath. Because there's no setter, assigning to it is refused outright:

What happens when you run this?
class Account:
    def __init__(self, balance=0):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

a = Account(50)
a.balance = 999

Nothing 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:

python

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:

step through it
1class Account:
2 def __init__(self, balance):
3 self.balance = balance
4 
5 def withdraw(self, amount):
6 if amount > self.balance:
7 return False
8 self.balance -= amount
9 return True
10 
11a = Account(100)
12ok1 = a.withdraw(30)
13ok2 = a.withdraw(500)
14print(a.balance, ok1, ok2)

🎯 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. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upRefuse a bad value

Define Thermostat starting at 20 with set_to(degrees) that ignores anything outside 5–30. Write final_temp(values) applying each and returning the temperature.

encapsulationguard
DrillThe underscore convention

Define Wallet storing its money in self._balance (leading underscore = internal), with add(amount) and a peek() returning it. Write wallet_after(amounts) adding each and returning the total.

encapsulationnaming
BuildA computed property

Define Circle taking radius, with area as a property (@property) returning 3.14159 * radius ** 2. Write circle_area(r) returning c.area — note: no parentheses.

encapsulationproperty
BossReject it at the door

Define Account whose __init__ raises ValueError if the opening balance is negative. Write open_account(amount) returning the balance, or -1 if opening was refused (catch the error).

encapsulationraiseguard
CapstoneThe account that can't go negative

Define Account(owner, balance) with deposit(amount) and withdraw(amount) returning True/False. Write statement(owner, start, moves) where each move is ["deposit", 50] or ["withdraw", 30], returning {"balance": ..., "refused": n} — the closing balance and how many moves were refused.

encapsulationguardloopdict
safe_withdraw.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
safe_withdraw(start, amounts) → floatApply each withdrawal that the account allows; return the balance.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.