Skip to content

Encapsulation and Access Modifiers

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.

Python uses naming conventions to indicate the intended access level of 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

Single underscore = “protected by convention”

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

Double underscore = “name-mangled, intended private”

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"
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"

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_setters.py
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") # Verbose
print(user.get_password()) # Verbose
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

Properties can also compute values on-the-fly:

computed_properties.py
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 access

You can create read-only properties by omitting the setter:

readonly_property.py
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 attribute
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

Methods can also be protected or private:

method_access.py
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([]) # AttributeError

Real-World Example: User Management System

Section titled “Real-World Example: User Management System”
user_management.py
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)
# Usage
user = UserAccount("john_doe", "john@example.com", "secure123")
user.email = "newemail@example.com" # Uses setter
print(user.email) # Uses getter
# print(user.password) # AttributeError
user.change_password("secure123", "newsecure456") # Valid
  • 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.