Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Encapsulation

Protect your data and control access with encapsulation.

Encapsulation is one of the fundamental principles of Object-Oriented Programming. It involves bundling data (attributes) and methods that operate on that data within a single unit (class), while restricting direct access to some components to prevent accidental modification.

Access modifiers control the visibility and accessibility of class members (attributes and methods).

access_levels.py
class User:
def __init__(self, username: str, email: str, password: str):
self.username = username # Public attribute
self._email = email # Protected attribute (convention)
self.__password = password # Private attribute (name mangling)
def public_method(self):
"""Public method - accessible from anywhere"""
return "Public method"
def _protected_method(self):
"""Protected method - intended for internal use"""
return "Protected method"
def __private_method(self):
"""Private method - name-mangled, harder to access"""
return "Private method"
Access LevelSyntaxEnforcementUse Case
PublicattributeNoneAccessible from anywhere
Protected_attributeConvention onlyInternal use, accessible but discouraged
Private__attributeName manglingTruly internal, harder to access accidentally

Protected members are intended for internal use but can be accessed by subclasses.

protected_example.py
class User:
def __init__(self, username: str, password: str):
self.username = username
self._password = password # Protected attribute
def get_password(self):
return self._password
def set_password(self, new_password: str):
self._password = new_password
user = User("john_doe", "secret123")
print(user._password) # Works, but not recommended
print(user.get_password()) # Preferred way

Private members are truly internal and should not be accessed from outside the class.

private_example.py
class User:
def __init__(self, username: str, password: str):
self.username = username
self.__password = password # Private attribute (name-mangled)
def get_password(self):
return self.__password # Accessible within class
user = User("john_doe", "secret123")
# print(user.__password) # AttributeError: 'User' object has no attribute '__password'
print(user.get_password()) # Works: "secret123"
# print(user._User__password) # Works but DON'T DO THIS: "secret123"

In Python, you usually don’t write getters/setters unless you need validation or transformation. Properties provide a clean way to add getters/setters.

properties_example.py
class User:
def __init__(self, username: str, email: str, password: str):
self.username = username
self.email = email
self._password = password # Protected attribute
@property
def password(self):
"""Getter - accessed like an attribute"""
return self._password
@password.setter
def password(self, new_password: str):
"""Setter - accessed like attribute assignment"""
if len(new_password) < 8:
raise ValueError("Password must be at least 8 characters")
self._password = new_password
user = User("john", "john@example.com", "old_password")
user.password = "new_password" # Clean syntax, uses setter
print(user.password) # Clean syntax, uses getter
# user.password = "short" # Raises ValueError
bad_encapsulation.py
class BankAccount:
def __init__(self, initial_balance: float):
self.balance = initial_balance # Public attribute - dangerous!
def deposit(self, amount: float):
self.balance += amount
def withdraw(self, amount: float):
self.balance -= amount
account = BankAccount(1000.0)
account.balance = -500.0 # Direct modification - unsafe!
print(account.balance) # -500.0 (incorrect in banking!)
good_encapsulation.py
class BankAccount:
def __init__(self, initial_balance: float):
self.__balance = initial_balance # Private attribute
def deposit(self, amount: float):
"""Deposit money with validation"""
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit amount must be positive")
def withdraw(self, amount: float):
"""Withdraw money with validation"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
@property
def balance(self):
"""Read-only access to balance"""
return self.__balance
account = BankAccount(1000.0)
account.deposit(500.0)
account.withdraw(200.0)
print(account.balance) # 1300.0
# account.__balance = -500.0 # Won't affect actual balance (name mangling)
# account.balance = -500.0 # AttributeError: can't set attribute
Diagram

Remember: In Python, encapsulation is more about convention and design than strict enforcement. In Java, encapsulation is enforced by the compiler. The goal is to create clear interfaces and prevent accidental misuse.