Python Encapsulation
Encapsulation is the OOP principle of bundling data (attributes) and methods that operate on that data together, while restricting direct access to some components. Python uses naming conventions rather than strict access modifiers.
Access Levels in Python
Python doesn't have private or protected keywords like Java or C++. Instead, it uses naming conventions:
| Convention | Syntax | Access Level |
|---|---|---|
| Public | name | Accessible everywhere |
| Protected | _name | Convention: internal use only |
| Private | __name | Name mangling applied |
python
class Employee:
def __init__(self, name, salary, ssn):
self.name = name # Public
self._salary = salary # Protected (convention)
self.__ssn = ssn # Private (name mangled)
emp = Employee("Alice", 85000, "123-45-6789")
# Public - accessible
print(emp.name) # Alice
# Protected - accessible but shouldn't be used externally
print(emp._salary) # 85000 (works, but discouraged)
# Private - name mangled
# print(emp.__ssn) # AttributeError!
print(emp._Employee__ssn) # 123-45-6789 (works, but never do this)Name Mangling
Python transforms __name to _ClassName__name to prevent accidental access:
python
class BankAccount:
def __init__(self, owner, balance):
self.__owner = owner
self.__balance = balance
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def __update_log(self):
"""Private method."""
print("Transaction logged")
account = BankAccount("Alice", 1000)
# Cannot access directly
# print(account.__balance) # AttributeError
# Use public methods instead
print(account.get_balance()) # 1000
# Name mangling: the attribute exists as _BankAccount__balance
print(dir(account)) # Shows _BankAccount__balance in the listProperties (Pythonic Encapsulation)
The @property decorator is the Pythonic way to implement encapsulation:
python
class User:
def __init__(self, name, email, age):
self._name = name
self._email = email
self.age = age # Uses the property setter
@property
def name(self):
"""Get name."""
return self._name
@name.setter
def name(self, value):
"""Set name with validation."""
if not value or not value.strip():
raise ValueError("Name cannot be empty")
self._name = value.strip()
@property
def email(self):
"""Get email."""
return self._email
@email.setter
def email(self, value):
"""Set email with validation."""
if "@" not in value:
raise ValueError("Invalid email address")
self._email = value.lower()
@property
def age(self):
"""Get age."""
return self._age
@age.setter
def age(self, value):
"""Set age with validation."""
if not isinstance(value, int) or value < 0 or value > 150:
raise ValueError("Age must be between 0 and 150")
self._age = value
@property
def info(self):
"""Read-only computed property."""
return f"{self._name} ({self._email}), age {self._age}"
# Usage - looks like regular attribute access
user = User("Alice", "alice@example.com", 30)
print(user.name) # Alice
print(user.email) # alice@example.com
print(user.info) # Alice (alice@example.com), age 30
# Setting with validation
user.name = " Bob "
print(user.name) # Bob (stripped)
user.email = "BOB@TEST.COM"
print(user.email) # bob@test.com (lowered)
# Validation catches errors
# user.age = -5 # ValueError: Age must be between 0 and 150
# user.email = "bad" # ValueError: Invalid email addressRead-Only Properties
Create attributes that can only be read, not set:
python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""Read-only: computed from radius."""
import math
return math.pi * self._radius ** 2
@property
def circumference(self):
"""Read-only: computed from radius."""
import math
return 2 * math.pi * self._radius
circle = Circle(5)
print(f"Area: {circle.area:.2f}") # Area: 78.54
print(f"Circumference: {circle.circumference:.2f}") # Circumference: 31.42
# Cannot set read-only properties
# circle.area = 100 # AttributeError: can't set attributeEncapsulation in Practice
python
class ShoppingCart:
"""
A shopping cart with proper encapsulation.
Internal state is protected; access through methods/properties.
"""
def __init__(self):
self._items = [] # Protected: internal list
self.__discount = 0 # Private: discount rate
@property
def items(self):
"""Return a copy of items (prevents external modification)."""
return self._items.copy()
@property
def total(self):
"""Calculate total with discount."""
subtotal = sum(item["price"] * item["quantity"] for item in self._items)
return subtotal * (1 - self.__discount)
@property
def item_count(self):
"""Total number of items."""
return sum(item["quantity"] for item in self._items)
def add_item(self, name, price, quantity=1):
"""Add item with validation."""
if price <= 0:
raise ValueError("Price must be positive")
if quantity <= 0:
raise ValueError("Quantity must be positive")
# Check if item already exists
for item in self._items:
if item["name"] == name:
item["quantity"] += quantity
return
self._items.append({
"name": name,
"price": price,
"quantity": quantity
})
def remove_item(self, name):
"""Remove an item by name."""
self._items = [i for i in self._items if i["name"] != name]
def apply_discount(self, percentage):
"""Apply discount with validation."""
if 0 <= percentage <= 50:
self.__discount = percentage / 100
else:
raise ValueError("Discount must be between 0% and 50%")
def display(self):
"""Display cart contents."""
print("π Shopping Cart")
print("-" * 40)
for item in self._items:
subtotal = item["price"] * item["quantity"]
print(f" {item['name']}: ${item['price']:.2f} Γ {item['quantity']} = ${subtotal:.2f}")
if self.__discount > 0:
print(f" Discount: {self.__discount * 100:.0f}%")
print(f" Total: ${self.total:.2f}")
print(f" Items: {self.item_count}")
# Usage
cart = ShoppingCart()
cart.add_item("Python Book", 39.99, 2)
cart.add_item("USB Cable", 12.99)
cart.add_item("Mouse Pad", 15.00)
cart.apply_discount(10)
cart.display()
# Cannot directly modify internal state
items = cart.items # Gets a copy
items.clear() # Only clears the copy
print(f"Cart still has {cart.item_count} items") # Still has itemsSummary
- Encapsulation bundles data and methods while restricting direct access
- Python uses naming conventions:
public,_protected,__private - Name mangling transforms
__attrto_ClassName__attr - Use
@propertyfor Pythonic getters, setters, and validation - Create read-only properties by omitting the setter
- Return copies of internal collections to prevent external modification
- Encapsulation protects data integrity and allows you to change internal implementation without affecting external code
Next, we'll learn about Python decorators.