Encapsulation
What Problem Does It Solve?
Encapsulation solves the problem of data hiding and protecting an object's internal state from unauthorized external access. Without it, any part of the codebase could directly modify an object's data, leading to unpredictable bugs, security vulnerabilities, and tight coupling between components.
Think of an ATM machine. You interact with it through a keypad, screen, and card slot — you cannot directly access the cash cassettes or the internal ledger. The ATM exposes a well-defined interface (withdraw, check balance) while keeping the internal mechanics private. Encapsulation works the same way: objects expose methods (the keypad/screen) and hide their internal data (the cash cassettes).
How to Identify When to Use It
- You have fields that should not be directly modifiable after object creation
- You need to validate data before assigning it (e.g., age can't be negative)
- Multiple parts of the code modify the same object's data, causing inconsistent state
- You want to change internal implementation without affecting external code
- You're exposing internal data structures directly via public fields
Questions to ask yourself:
- Can I trust that this object's state is always valid?
- What happens if someone sets an invalid value on this field?
- If I change how this data is stored, how many files do I need to update?
Red flags: Public attributes everywhere, direct mutation of collection fields from outside the class, no validation on state changes, "train wreck" method chains that reach deep into objects.
How to Apply It
- Mark instance variables as private (
self.__varin Python orself._varfor protected) - Provide public getter/setter methods if external access is needed
- Validate data in setters before assigning
- Use properties in Python for clean attribute-style access with validation
class BankAccount:
def __init__(self, owner: str, initial_balance: float = 0.0):
self.owner = owner
self.__balance = initial_balance # private attribute
self.__transaction_log: list[str] = []
# Getter — controlled read access
def get_balance(self) -> float:
return self.__balance
# Setter with validation
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
self.__transaction_log.append(f"Deposited: ${amount:.2f}")
def withdraw(self, amount: float) -> bool:
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
print("Insufficient funds")
return False
self.__balance -= amount
self.__transaction_log.append(f"Withdrew: ${amount:.2f}")
return True
def get_transaction_log(self) -> list[str]:
# Return a copy so the caller can't mutate the internal log
return list(self.__transaction_log)Real-World Example
A User profile class that protects email and password, ensuring the password is never exposed directly and the email is always valid.
import re
class UserProfile:
def __init__(self, username: str, email: str):
self.username = username
self.__email = email
self.__password_hash: str | None = None
@property
def email(self) -> str:
return self.__email
@email.setter
def email(self, new_email: str) -> None:
if not re.match(r"[^@]+@[^@]+\.[^@]+", new_email):
raise ValueError("Invalid email format")
self.__email = new_email
def set_password(self, password: str) -> None:
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
self.__password_hash = self.__hash(password) # never store raw
def __hash(self, password: str) -> str:
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(self, password: str) -> bool:
return self.__password_hash == self.__hash(password)The __password_hash is never directly accessible. The email setter validates format. The internal __hash method is implementation detail that can change without breaking callers.
Common Mistakes / Pitfalls
- Over-exposing internals: Adding getters/setters for every field defeats the purpose. Only expose what callers actually need.
- Returning mutable references: Returning
self.__itemsdirectly lets callers modify the internal list. Return a copy or usetuple(). - Using single underscore thinking it's private:
_varin Python is a convention, not enforced. Name mangling with__vargives real (though still accessible via_ClassName__var) protection. - God classes: A single class that "owns" everything is not encapsulation — it's a monolith. Encapsulation means each class owns its own state.
- Leaking implementation details in tests: Don't test private methods. Test the public interface.
