Encapsulation
Protect an object's internal state by funneling every change through methods that can enforce what's valid — and understand what Python's underscore convention does and doesn't do.
By the end of this lesson, you can
- Explain encapsulation as protecting an object's state by controlling how it can change
- Use a leading underscore to signal that an attribute is meant to be internal
- Explain why Python's underscore convention is a signal, not enforcement, and what that means in practice
Why it matters
An account balance should never go negative from a withdrawal that
exceeds it. If any code, anywhere, can set account.balance to
whatever it wants, that rule only holds as long as everyone remembers
to check it manually — which is exactly the kind of thing that
eventually gets forgotten. Encapsulation protects a rule like this
by making it impossible to change the balance except through a method
that enforces it.
Mental model
The previous lesson distinguished an object’s public interface from its
internal details. Encapsulation goes one step further: it protects an
object’s internal state by funneling every change through methods,
which can enforce rules a direct assignment never would. Python signals
“this is internal” with a leading underscore, like _balance.
class Account:
def __init__(self, starting_balance):
self._balance = starting_balance
def deposit(self, amount):
self._balance = self._balance + amount
def withdraw(self, amount):
if amount > self._balance:
return False
self._balance = self._balance - amount
return True
def balance(self):
return self._balance
account = Account(100)
print(account.withdraw(150))
print(account.balance())False
100withdraw() refuses to let the balance go negative — the rule is
enforced every single time, because there’s no other way to change
_balance through the interface.
Trace it
| Call | Allowed? | _balance afterward |
|---|---|---|
| Account(100) | — | 100 |
| withdraw(150) | No — exceeds balance | 100 (unchanged) |
| deposit(50) | Yes | 150 |
| withdraw(150) | Yes — exactly enough | 0 |
Every single change went through deposit or withdraw, and every one
of those calls was checked against the current balance.
Check your understanding
Or reveal the answer without checking
Answer:No — Python allows setting any attribute directly, including ones with a leading underscore
A leading underscore is purely a naming convention in Python — it carries no enforcement. Setting account._balance directly works exactly like setting any other attribute.
Or reveal the answer without checking
Answer:It protects state as long as other code goes through the public interface, which is a discipline worth signaling clearly, even without language-level enforcement
Encapsulation in Python is a design discipline: methods that enforce rules protect state for all the code that respects the interface, which in practice is almost everything — the underscore just makes the boundary clear.
Practice: warm-up
Trace this program — write down _balance after each call, and whether
each withdraw succeeds.
account = Account(50)
account.deposit(20)
account.withdraw(60)
account.withdraw(10)
Stuck? Reveal one hint at a time.
Hint 1
Work through deposit and withdraw calls in order, updating the running balance only when a call actually succeeds.
Hint 2
withdraw(60) needs to be checked against the balance at that point in the trace, not the starting balance.
Reveal the trace
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Account(50) -> _balance = 50
deposit(20) -> _balance = 70
withdraw(60) -> 60 <= 70, allowed -> _balance = 10
withdraw(10) -> 10 <= 10, allowed -> _balance = 0Practice: apply it
This code needs to give a customer a $500 credit, but does it in a way that skips the account’s own rules:
account = Account(100)
account._balance = account._balance + 500
Or reveal the answer without checking
Answer:It bypasses deposit() entirely, so if deposit() is ever changed to also log transactions or cap deposit size, this code silently ignores that change
Reaching into _balance directly works today, but it means any rule or behavior added to deposit() later — logging, limits, validation — never applies to this code, since it never goes through deposit() at all.
Modification challenge: rewrite this to use account.deposit(500)
instead of setting _balance directly.
Summary
- Encapsulation protects an object’s state by funneling every change through methods that can enforce rules — like refusing to let a balance go negative.
- A leading underscore, as in
_balance, is Python’s convention for signaling that an attribute is internal and shouldn’t be set directly. - Python does not enforce the underscore convention — code can bypass it, which silently skips whatever rules the proper method would have enforced.
- Encapsulation in Python is a design discipline that depends on code respecting the interface, not a guarantee the language provides for free.