Encapsulation and Access Modifiers
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.
Understanding Access Modifiers
Section titled “Understanding Access Modifiers”Python uses naming conventions to indicate the intended access level of attributes and methods:
Access Levels
Section titled “Access Levels”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"Key Differences
Section titled “Key Differences”| Access Level | Syntax | Enforcement | Use Case |
|---|---|---|---|
| Public | attribute | None | Accessible from anywhere |
| Protected | _attribute | Convention only | Internal use, accessible but discouraged |
| Private | __attribute | Name mangling | Truly internal, harder to access accidentally |
Protected Attributes (_attribute)
Section titled “Protected Attributes (_attribute)”Single underscore = “protected by convention”
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 recommendedprint(user.get_password()) # Preferred wayPrivate Attributes (__attribute)
Section titled “Private Attributes (__attribute)”Double underscore = “name-mangled, intended private”
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"classDiagram
class User {
+username: str
-_email: str (protected)
-__password: str (private)
+get_password() str
+set_password(str)
}
note for User "Protected: Convention only\nPrivate: Name-mangled"
Properties: The Pythonic Way
Section titled “Properties: The Pythonic Way”In Python, you usually don’t write getters/setters unless:
- You need validation or transformation
- You want to keep the API stable while changing internals
Otherwise, direct attribute access is idiomatic.
Traditional Getters and Setters
Section titled “Traditional Getters and Setters”class User: def __init__(self, username: str, email: str, password: str): self.username = username self.email = email self._password = password
def get_password(self): return self._password
def set_password(self, new_password: str): self._password = new_password
user = User("john", "john@example.com", "old_password")user.set_password("new_password") # Verboseprint(user.get_password()) # VerboseUsing Properties (Pythonic Way)
Section titled “Using Properties (Pythonic Way)”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 setterprint(user.password) # Clean syntax, uses getter# user.password = "short" # Raises ValueErrorComputed Properties
Section titled “Computed Properties”Properties can also compute values on-the-fly:
class User: def __init__(self, first_name: str, last_name: str): self.first_name = first_name self.last_name = last_name
@property def full_name(self): """Computed property - combines first and last name""" return f"{self.first_name} {self.last_name}"
@property def email_domain(self): """Computed property - extracts domain from email""" if hasattr(self, '_email'): return self._email.split('@')[1] if '@' in self._email else None return None
user = User("John", "Doe")print(user.full_name) # "John Doe" - computed on access
user._email = "john@example.com"print(user.email_domain) # "example.com" - computed on accessRead-Only Properties
Section titled “Read-Only Properties”You can create read-only properties by omitting the setter:
class User: def __init__(self, username: str, user_id: int): self.username = username self._user_id = user_id # Private attribute
@property def user_id(self): """Read-only property - no setter defined""" return self._user_id
user = User("john_doe", 12345)print(user.user_id) # Works: 12345# user.user_id = 67890 # AttributeError: can't set attributeEncapsulation in Practice
Section titled “Encapsulation in Practice”Bad Example: No Encapsulation
Section titled “Bad Example: No Encapsulation”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 Example: Proper Encapsulation
Section titled “Good Example: Proper Encapsulation”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 attributeProtected and Private Methods
Section titled “Protected and Private Methods”Methods can also be protected or private:
class DataProcessor: def __init__(self, data: list): self.data = data
def process(self): """Public method - main interface""" cleaned = self._clean_data() # Call protected method validated = self.__validate_data(cleaned) # Call private method return validated
def _clean_data(self): """Protected method - internal use""" return [item.strip() for item in self.data if item]
def __validate_data(self, data: list): """Private method - truly internal""" return [item for item in data if len(item) > 0]
processor = DataProcessor([" item1 ", "item2", " "])result = processor.process() # Public method# processor._clean_data() # Works but not recommended# processor.__validate_data([]) # AttributeErrorReal-World Example: User Management System
Section titled “Real-World Example: User Management System”class UserAccount: """Complete example demonstrating encapsulation""" def __init__(self, username: str, email: str, password: str): self.username = username # Public self._email = email # Protected self.__password = self._hash_password(password) # Private
def _hash_password(self, password: str) -> str: """Protected method - internal password hashing""" import hashlib return hashlib.sha256(password.encode()).hexdigest()
@property def email(self): """Getter for email""" return self._email
@email.setter def email(self, value: str): """Setter with validation""" if "@" not in value: raise ValueError("Invalid email address") self._email = value
@property def password(self): """Read-only - password hash cannot be retrieved""" raise AttributeError("Password cannot be read")
def verify_password(self, password: str) -> bool: """Verify password without exposing it""" hashed = self._hash_password(password) return hashed == self.__password
def change_password(self, old_password: str, new_password: str): """Change password with verification""" if not self.verify_password(old_password): raise ValueError("Incorrect password") if len(new_password) < 8: raise ValueError("Password must be at least 8 characters") self.__password = self._hash_password(new_password)
# Usageuser = UserAccount("john_doe", "john@example.com", "secure123")user.email = "newemail@example.com" # Uses setterprint(user.email) # Uses getter# print(user.password) # AttributeErroruser.change_password("secure123", "newsecure456") # ValidKey Takeaways
Section titled “Key Takeaways”- Encapsulation bundles data and methods together while controlling access
- Protected attributes (
_attr) are convention-based - Python doesn’t enforce them - Private attributes (
__attr) use name mangling - harder to access accidentally - Properties provide a pythonic way to add getters/setters with validation
- Use properties when you need validation, transformation, or API stability
- Direct attribute access is idiomatic when no special handling is needed
- Private methods (
__method) are truly internal and harder to access from outside
Remember: In Python, encapsulation is more about convention and design than strict enforcement. The goal is to create clear interfaces and prevent accidental misuse.