Python - Encapsulation (Public, Private, Protected)
• Python uses naming conventions rather than strict access modifiers—single underscore (_) for protected, double underscore (__) for private, and no prefix for public attributes
Key Insights
• Python uses naming conventions rather than strict access modifiers—single underscore (_) for protected, double underscore (__) for private, and no prefix for public attributes • Name mangling with double underscores provides obfuscation, not true security, as attributes remain accessible through _ClassName__attribute syntax • Property decorators (@property) offer the most Pythonic way to implement encapsulation while maintaining clean syntax and backward compatibility
Understanding Python’s Encapsulation Model
Python takes a fundamentally different approach to encapsulation compared to languages like Java or C++. Instead of enforcing access control through language keywords, Python relies on naming conventions and a philosophy summarized as “we’re all consenting adults here.” This doesn’t mean encapsulation is absent—it’s just implemented through convention and social contract rather than compiler enforcement.
The three levels of access in Python are distinguished by naming patterns:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number # Public
self._owner_id = None # Protected
self.__pin = "1234" # Private
Public Attributes and Methods
Public members have no prefix and are meant to be accessed freely by any code. They form the official API of your class.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
self.inventory_count = 0
def restock(self, quantity):
self.inventory_count += quantity
return self.inventory_count
def get_total_value(self):
return self.price * self.inventory_count
# Usage
product = Product("Laptop", 999.99)
product.restock(50)
print(f"{product.name}: ${product.get_total_value()}") # Laptop: $49999.50
Public attributes can be accessed and modified directly. This is intentional—Python developers prefer explicit interfaces over hidden complexity.
Protected Attributes (Single Underscore)
Protected attributes use a single leading underscore. This is a convention signaling “internal use—proceed with caution.” The interpreter doesn’t enforce any restrictions, but it’s a clear message to other developers.
class DatabaseConnection:
def __init__(self, connection_string):
self._connection_string = connection_string
self._connection = None
self._is_connected = False
def connect(self):
# Simulate connection
self._connection = f"Connected to {self._connection_string}"
self._is_connected = True
self._log_connection()
def _log_connection(self):
"""Protected method for internal logging"""
print(f"Connection established: {self._connection_string}")
def disconnect(self):
if self._is_connected:
self._connection = None
self._is_connected = False
# Usage
db = DatabaseConnection("postgresql://localhost:5432/mydb")
db.connect()
# Technically accessible, but convention says don't do this
print(db._connection_string) # Works, but violates convention
Protected members are particularly useful in inheritance scenarios where subclasses need access to internal state:
class SecureConnection(DatabaseConnection):
def __init__(self, connection_string, ssl_cert):
super().__init__(connection_string)
self._ssl_cert = ssl_cert
def connect(self):
# Override and extend parent behavior
self._validate_certificate()
super().connect()
def _validate_certificate(self):
"""Subclass can access parent's protected members"""
print(f"Validating SSL for {self._connection_string}")
Private Attributes (Double Underscore)
Private attributes use a double leading underscore, triggering name mangling. Python transforms __attribute into _ClassName__attribute, making accidental access from outside the class less likely.
class CreditCard:
def __init__(self, card_number, cvv):
self.__card_number = card_number
self.__cvv = cvv
self.cardholder_name = ""
def charge(self, amount):
if self.__validate_card():
return f"Charged ${amount} to card ending in {self.__get_last_four()}"
return "Invalid card"
def __validate_card(self):
"""Private method - name mangled"""
return len(self.__card_number) == 16
def __get_last_four(self):
return self.__card_number[-4:]
# Usage
card = CreditCard("1234567890123456", "123")
print(card.charge(99.99)) # Works fine
# These raise AttributeError
# print(card.__card_number)
# card.__validate_card()
# But name mangling isn't true privacy
print(card._CreditCard__card_number) # 1234567890123456 - still accessible!
Name mangling primarily prevents accidental attribute collisions in inheritance hierarchies:
class Parent:
def __init__(self):
self.__private = "parent"
def get_private(self):
return self.__private
class Child(Parent):
def __init__(self):
super().__init__()
self.__private = "child" # Different attribute due to name mangling
def get_both(self):
return f"Child: {self.__private}, Parent: {self.get_private()}"
child = Child()
print(child.get_both()) # Child: child, Parent: parent
Property Decorators: The Pythonic Approach
The most elegant encapsulation in Python uses property decorators, providing controlled access while maintaining attribute-like syntax.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Getter for celsius"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter with validation"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Computed property"""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
# Usage
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 32
print(temp.celsius) # 0.0
# temp.celsius = -300 # Raises ValueError
Properties enable you to start with simple public attributes and add validation later without breaking existing code:
class User:
def __init__(self, email):
self._email = None
self.email = email # Uses setter for validation
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if '@' not in value:
raise ValueError("Invalid email address")
self._email = value.lower()
@property
def domain(self):
"""Read-only computed property"""
return self._email.split('@')[1]
user = User("John.Doe@Example.COM")
print(user.email) # john.doe@example.com
print(user.domain) # example.com
# user.domain = "other.com" # AttributeError: can't set attribute
Practical Encapsulation Patterns
Combine these techniques for robust class design:
class ShoppingCart:
def __init__(self):
self._items = []
self.__discount_rate = 0.0
def add_item(self, item, price, quantity=1):
"""Public interface"""
self._items.append({
'item': item,
'price': price,
'quantity': quantity
})
@property
def subtotal(self):
return sum(item['price'] * item['quantity'] for item in self._items)
@property
def total(self):
return self.subtotal * (1 - self.__discount_rate)
def apply_discount(self, code):
"""Public method using private validation"""
if self.__validate_discount_code(code):
self.__discount_rate = 0.1
return True
return False
def __validate_discount_code(self, code):
"""Private validation logic"""
return code == "SAVE10"
cart = ShoppingCart()
cart.add_item("Widget", 29.99, 2)
cart.add_item("Gadget", 49.99)
print(f"Subtotal: ${cart.subtotal:.2f}") # Subtotal: $109.97
cart.apply_discount("SAVE10")
print(f"Total: ${cart.total:.2f}") # Total: $98.97
Python’s encapsulation model prioritizes flexibility and clarity over rigid enforcement. Use public attributes for your API, protected for internal implementation details, private for avoiding name collisions, and properties for controlled access with validation. The goal isn’t to hide everything—it’s to communicate intent clearly to other developers.