OOP: The Reality of Encapsulation
OOP didn't invent encapsulation — C had perfect encapsulation before C++ and Java actually weakened it with header file requirements.
Why this matters
Encapsulation is often cited as one of OOP's defining contributions, but Robert C. Martin challenges this. C programs, compiled from a .c file with a .h header, gave users access only to function signatures — the implementation was completely hidden. That is perfect encapsulation. When C++ introduced header files that had to declare private member variables, and Java required all class source to be visible, the language-enforced encapsulation Martin calls "perfect" was actually degraded.
The lesson is that encapsulation is not a language feature — it is a discipline. The language can make it easier or harder to break, but it cannot enforce it. A developer writing Python or TypeScript must choose to protect internal state through naming conventions, properties, and private keywords. Without that discipline, objects become transparent data buckets that anyone can corrupt.
✗The problem
When all attributes are public, callers can assign any value to any field. Invariants — like "balance must never be negative" — cannot be enforced.
Bad
class BankAccount:
def __init__(self, owner: str, balance: float):
self.owner = owner
self.balance = balance # nothing stops: acc.balance = -999999
account = BankAccount("Alice", 500.0)
account.balance = -99999 # silent corruption — no validation possible
class BankAccount {
owner: string;
balance: number; // any caller can write: acc.balance = -Infinity
constructor(owner: string, balance: number) {
this.owner = owner;
this.balance = balance;
}
}
const acc = new BankAccount("Alice", 500);
acc.balance = -99999; // no guard, no error — invariant broken silently
✓The solution
Private attributes and controlled methods enforce invariants at the boundary. State changes only happen through operations that validate the input first.
Good
class BankAccount:
def __init__(self, owner: str, initial: float):
self._owner = owner
self._balance = initial
@property
def balance(self) -> float: return self._balance
def deposit(self, amount: float) -> None:
if amount <= 0: raise ValueError("Must be positive")
self._balance += amount
def withdraw(self, amount: float) -> None:
if amount <= 0: raise ValueError("Must be positive")
if amount > self._balance: raise ValueError("Insufficient funds")
self._balance -= amount
acc = BankAccount("Alice", 500.0)
acc.deposit(100.0)
print(acc.balance) # 600.0
class BankAccount {
private _balance: number;
private _owner: string;
constructor(owner: string, initialBalance: number) {
this._owner = owner;
this._balance = initialBalance;
}
get balance(): number { return this._balance; }
deposit(amount: number): void {
if (amount <= 0) throw new Error("Must be positive");
this._balance += amount;
}
withdraw(amount: number): void {
if (amount <= 0) throw new Error("Must be positive");
if (amount > this._balance) throw new Error("Insufficient funds");
this._balance -= amount;
}
}
const acc = new BankAccount("Alice", 500);
acc.deposit(100);
console.log(acc.balance); // 600
💡Key takeaway
Encapsulation is a discipline enforced by the programmer, not a guarantee provided by the language — the language merely makes it easier or harder to break.
🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.
Hint: Encapsulation is a discipline enforced by the programmer, not guaranteed by the language.
✗ Your version