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.