Object-Oriented Programming

Python Encapsulation

Learn about encapsulation in Python - access modifiers, name mangling, properties, and data hiding for robust class design.

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:

ConventionSyntaxAccess Level
PublicnameAccessible everywhere
Protected_nameConvention: internal use only
Private__nameName 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 list

Properties (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 address

Read-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 attribute

Encapsulation 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 items

Summary

  • Encapsulation bundles data and methods while restricting direct access
  • Python uses naming conventions: public, _protected, __private
  • Name mangling transforms __attr to _ClassName__attr
  • Use @property for 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.