Daily Tech Brief

Top startup stories in your inbox

Subscribe Free

© 2026 rakrisi Daily

Encapsulation - Protecting Your Data

Encapsulation: Protecting Your Data

Welcome to Encapsulation! Think of it as putting your code in a protective capsule - hiding internal details and controlling access to your object’s data.

What is Encapsulation?

Encapsulation is about bundling data and methods together and restricting access to internal details. It’s like a black box - you know what it does, but not how it works internally.

Without Encapsulation (Dangerous!)

class BankAccount:
    def __init__(self, balance):
        self.balance = balance  # Anyone can change this directly!

account = BankAccount(1000)
account.balance = -500  # This should not be allowed!
print(account.balance)  # -500 - Oh no!

With Encapsulation (Safe!)

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # "Private" attribute

    def get_balance(self):
        return self._balance

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount

account = BankAccount(1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance())  # 1300 - Safe!

# account._balance = -500  # Still possible but discouraged

Public vs Private Attributes

Naming Conventions

class Person:
    def __init__(self, name, age, salary):
        self.name = name          # Public attribute
        self._age = age          # Protected attribute (convention)
        self.__salary = salary   # Private attribute (name mangling)

    def get_age(self):
        return self._age

    def set_age(self, age):
        if 0 <= age <= 150:
            self._age = age

    def get_salary(self):
        return self.__salary

    def give_raise(self, amount):
        if amount > 0:
            self.__salary += amount

# Test
person = Person("Alice", 30, 50000)

print(person.name)        # "Alice" - Public access
print(person.get_age())   # 30 - Controlled access
print(person.get_salary()) # 50000 - Controlled access

# Direct access (discouraged)
print(person._age)        # 30 - Convention only
# print(person.__salary)   # AttributeError - Name mangled to _Person__salary
print(person._Person__salary)  # 50000 - But don't do this!

Name Mangling

class Example:
    def __init__(self):
        self.__private = "secret"
        self._protected = "semi-secret"
        self.public = "open"

obj = Example()

print(obj.public)           # "open"
print(obj._protected)       # "semi-secret" (convention)
# print(obj.__private)      # AttributeError
print(obj._Example__private) # "secret" (name mangling)

Property Decorators

The @property Decorator

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        """Get temperature in Celsius."""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """Set temperature in Celsius with validation."""
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value

    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit."""
        return (self._celsius * 9/5) + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        """Set temperature in Fahrenheit."""
        celsius = (value - 32) * 5/9
        self.celsius = celsius  # Uses the celsius setter for validation

# Test
temp = Temperature(20)

print(f"Celsius: {temp.celsius}")      # 20
print(f"Fahrenheit: {temp.fahrenheit}") # 68.0

temp.celsius = 25
print(f"New Celsius: {temp.celsius}")  # 25

temp.fahrenheit = 100
print(f"After setting Fahrenheit: {temp.celsius}")  # 37.777...

try:
    temp.celsius = -300  # Below absolute zero
except ValueError as e:
    print(f"Error: {e}")

Read-Only Properties

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 property - calculated from radius."""
        return 3.14159 * self._radius ** 2

    @property
    def circumference(self):
        """Read-only property."""
        return 2 * 3.14159 * self._radius

# Test
circle = Circle(5)

print(f"Radius: {circle.radius}")          # 5
print(f"Area: {circle.area:.2f}")          # 78.54
print(f"Circumference: {circle.circumference:.2f}")  # 31.42

circle.radius = 10
print(f"New area: {circle.area:.2f}")      # 314.16

# circle.area = 100  # AttributeError - can't set read-only property

Data Validation and Business Rules

Validation in Setters

class Email:
    def __init__(self, address):
        self.address = address  # Uses setter

    @property
    def address(self):
        return self._address

    @address.setter
    def address(self, value):
        if not isinstance(value, str):
            raise TypeError("Email must be a string")
        if "@" not in value:
            raise ValueError("Invalid email format")
        if value.count("@") != 1:
            raise ValueError("Email must contain exactly one @")
        self._address = value.lower().strip()

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value or not value.strip():
            raise ValueError("Name cannot be empty")
        if len(value.strip()) < 2:
            raise ValueError("Name must be at least 2 characters")
        self._name = value.strip().title()

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise TypeError("Age must be an integer")
        if value < 0:
            raise ValueError("Age cannot be negative")
        if value > 150:
            raise ValueError("Age seems unreasonably high")
        self._age = value

# Test
try:
    user = User("alice", "alice@example.com", 25)
    print(f"User: {user.name}, Email: {user.email}, Age: {user.age}")

    user.name = "bob"  # Valid change
    user.age = 30      # Valid change

    user.name = ""     # Invalid - empty name
except ValueError as e:
    print(f"Validation error: {e}")

Business Logic Encapsulation

class BankAccount:
    def __init__(self, account_number, owner, initial_balance=0):
        self._account_number = account_number
        self._owner = owner
        self._balance = initial_balance
        self._transactions = []

    @property
    def account_number(self):
        return self._account_number

    @property
    def owner(self):
        return self._owner

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        """Deposit money with validation."""
        if not isinstance(amount, (int, float)):
            raise TypeError("Amount must be a number")
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")

        self._balance += amount
        self._record_transaction("deposit", amount)

    def withdraw(self, amount):
        """Withdraw money with business rules."""
        if not isinstance(amount, (int, float)):
            raise TypeError("Amount must be a number")
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")

        # Business rule: Maximum withdrawal of $1000 per transaction
        if amount > 1000:
            raise ValueError("Maximum withdrawal is $1000")

        self._balance -= amount
        self._record_transaction("withdrawal", amount)

    def transfer(self, target_account, amount):
        """Transfer money to another account."""
        # Validate target account
        if not isinstance(target_account, BankAccount):
            raise TypeError("Target must be a BankAccount")

        # Use existing withdrawal logic
        self.withdraw(amount)
        target_account.deposit(amount)

        self._record_transaction("transfer_out", amount, target_account.account_number)
        target_account._record_transaction("transfer_in", amount, self.account_number)

    def _record_transaction(self, transaction_type, amount, other_account=None):
        """Record a transaction internally."""
        transaction = {
            "type": transaction_type,
            "amount": amount,
            "balance_after": self._balance,
            "other_account": other_account
        }
        self._transactions.append(transaction)

    def get_transaction_history(self):
        """Get transaction history (read-only)."""
        return self._transactions.copy()  # Return copy to prevent modification

# Test
account1 = BankAccount("12345", "Alice", 1000)
account2 = BankAccount("67890", "Bob", 500)

account1.deposit(200)
account1.withdraw(150)
account1.transfer(account2, 100)

print(f"Account 1 balance: ${account1.balance}")
print(f"Account 2 balance: ${account2.balance}")

print("Account 1 transactions:")
for tx in account1.get_transaction_history():
    print(f"  {tx['type']}: ${tx['amount']} (Balance: ${tx['balance_after']})")

Class Variables and Methods

Class Variables with Encapsulation

class Employee:
    # Class variables (shared by all instances)
    _company_name = "Tech Corp"
    _total_employees = 0
    _next_id = 1000

    def __init__(self, name, salary):
        self._id = Employee._next_id
        Employee._next_id += 1

        self._name = name
        self._salary = salary
        Employee._total_employees += 1

    @classmethod
    def get_total_employees(cls):
        """Class method to get total employees."""
        return cls._total_employees

    @classmethod
    def set_company_name(cls, name):
        """Class method to set company name."""
        cls._company_name = name

    @classmethod
    def get_company_name(cls):
        """Class method to get company name."""
        return cls._company_name

    @property
    def id(self):
        return self._id

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value.strip():
            raise ValueError("Name cannot be empty")
        self._name = value.strip()

    @property
    def salary(self):
        return self._salary

    @salary.setter
    def salary(self, value):
        if value < 0:
            raise ValueError("Salary cannot be negative")
        self._salary = value

# Test
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 60000)

print(f"Total employees: {Employee.get_total_employees()}")
print(f"Company: {Employee.get_company_name()}")

Employee.set_company_name("Super Tech Corp")
print(f"New company: {Employee.get_company_name()}")

Practical Examples

Example 1: Password Manager

import hashlib
import secrets

class PasswordManager:
    def __init__(self):
        self._passwords = {}  # service -> hashed_password
        self._master_password = None
        self._salt = secrets.token_hex(16)

    def set_master_password(self, password):
        """Set master password with hashing."""
        if len(password) < 8:
            raise ValueError("Master password must be at least 8 characters")
        self._master_password = self._hash_password(password)

    def _hash_password(self, password):
        """Hash a password with salt."""
        salted = password + self._salt
        return hashlib.sha256(salted.encode()).hexdigest()

    def _verify_master_password(self, password):
        """Verify master password."""
        return self._hash_password(password) == self._master_password

    def add_password(self, service, password, master_password):
        """Add a password for a service."""
        if not self._verify_master_password(master_password):
            raise ValueError("Invalid master password")

        if service in self._passwords:
            raise ValueError(f"Password for {service} already exists")

        self._passwords[service] = self._hash_password(password)
        print(f"Password added for {service}")

    def get_password(self, service, master_password):
        """Retrieve a password for a service."""
        if not self._verify_master_password(master_password):
            raise ValueError("Invalid master password")

        if service not in self._passwords:
            raise ValueError(f"No password found for {service}")

        return self._passwords[service]

    def list_services(self, master_password):
        """List all services."""
        if not self._verify_master_password(master_password):
            raise ValueError("Invalid master password")

        return list(self._passwords.keys())

# Test
pm = PasswordManager()
pm.set_master_password("mysecretpassword")

pm.add_password("gmail", "gmailpass123", "mysecretpassword")
pm.add_password("github", "githubpass456", "mysecretpassword")

print("Services:", pm.list_services("mysecretpassword"))

# This would fail
try:
    pm.get_password("gmail", "wrongpassword")
except ValueError as e:
    print(f"Error: {e}")

Example 2: Configuration Manager

import json
import os
from pathlib import Path

class ConfigManager:
    def __init__(self, config_file="config.json"):
        self._config_file = Path(config_file)
        self._config = self._load_config()
        self._defaults = {
            "debug": False,
            "max_connections": 10,
            "timeout": 30,
            "theme": "light"
        }

    def _load_config(self):
        """Load configuration from file."""
        if not self._config_file.exists():
            return self._defaults.copy()

        try:
            with open(self._config_file, "r") as f:
                config = json.load(f)
                # Merge with defaults for missing keys
                merged = self._defaults.copy()
                merged.update(config)
                return merged
        except (json.JSONDecodeError, IOError):
            print("Warning: Could not load config file, using defaults")
            return self._defaults.copy()

    def _save_config(self):
        """Save configuration to file."""
        try:
            self._config_file.parent.mkdir(parents=True, exist_ok=True)
            with open(self._config_file, "w") as f:
                json.dump(self._config, f, indent=2)
        except IOError as e:
            print(f"Warning: Could not save config: {e}")

    @property
    def debug(self):
        return self._config["debug"]

    @debug.setter
    def debug(self, value):
        if not isinstance(value, bool):
            raise TypeError("Debug must be a boolean")
        self._config["debug"] = value
        self._save_config()

    @property
    def max_connections(self):
        return self._config["max_connections"]

    @max_connections.setter
    def max_connections(self, value):
        if not isinstance(value, int) or value < 1:
            raise ValueError("Max connections must be a positive integer")
        self._config["max_connections"] = value
        self._save_config()

    @property
    def timeout(self):
        return self._config["timeout"]

    @timeout.setter
    def timeout(self, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError("Timeout must be a positive number")
        self._config["timeout"] = value
        self._save_config()

    @property
    def theme(self):
        return self._config["theme"]

    @theme.setter
    def theme(self, value):
        valid_themes = ["light", "dark", "auto"]
        if value not in valid_themes:
            raise ValueError(f"Theme must be one of: {valid_themes}")
        self._config["theme"] = value
        self._save_config()

    def get_all_settings(self):
        """Get all settings as a dictionary."""
        return self._config.copy()

    def reset_to_defaults(self):
        """Reset all settings to defaults."""
        self._config = self._defaults.copy()
        self._save_config()

# Test
config = ConfigManager()

print("Initial settings:")
print(config.get_all_settings())

config.debug = True
config.max_connections = 20
config.theme = "dark"

print("\nAfter changes:")
print(config.get_all_settings())

try:
    config.theme = "invalid"
except ValueError as e:
    print(f"Error: {e}")

Example 3: Inventory System

class InventoryItem:
    def __init__(self, item_id, name, price, quantity=0):
        self._item_id = item_id
        self._name = name
        self._price = price
        self._quantity = quantity

    @property
    def item_id(self):
        return self._item_id

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value.strip():
            raise ValueError("Name cannot be empty")
        self._name = value.strip()

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, (int, float)) or value < 0:
            raise ValueError("Price must be a non-negative number")
        self._price = value

    @property
    def quantity(self):
        return self._quantity

    @quantity.setter
    def quantity(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("Quantity must be a non-negative integer")
        self._quantity = value

    @property
    def total_value(self):
        """Calculated property - read-only."""
        return self._price * self._quantity

    def add_stock(self, amount):
        """Add to inventory."""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self.quantity = self._quantity + amount

    def remove_stock(self, amount):
        """Remove from inventory."""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        if amount > self._quantity:
            raise ValueError("Insufficient stock")
        self.quantity = self._quantity - amount

class Inventory:
    def __init__(self):
        self._items = {}  # item_id -> InventoryItem

    def add_item(self, item):
        """Add an item to inventory."""
        if item.item_id in self._items:
            raise ValueError(f"Item {item.item_id} already exists")
        self._items[item.item_id] = item

    def get_item(self, item_id):
        """Get an item by ID."""
        if item_id not in self._items:
            raise ValueError(f"Item {item_id} not found")
        return self._items[item_id]

    def update_quantity(self, item_id, new_quantity):
        """Update item quantity."""
        item = self.get_item(item_id)
        item.quantity = new_quantity

    @property
    def total_value(self):
        """Total value of all inventory."""
        return sum(item.total_value for item in self._items.values())

    def get_low_stock_items(self, threshold=5):
        """Get items with low stock."""
        return [item for item in self._items.values() if item.quantity <= threshold]

# Test
inventory = Inventory()

item1 = InventoryItem("001", "Laptop", 999.99, 10)
item2 = InventoryItem("002", "Mouse", 25.50, 50)

inventory.add_item(item1)
inventory.add_item(item2)

print(f"Total inventory value: ${inventory.total_value:.2f}")

item1.add_stock(5)
item2.remove_stock(10)

print(f"Low stock items: {[item.name for item in inventory.get_low_stock_items()]}")

Practice Exercises

Exercise 1: Student Grade Manager

Create a Student class with:

  • Encapsulated grades list
  • Methods to add grades with validation
  • Property for average grade (read-only)
  • Property for letter grade (calculated)

Exercise 2: Secure Password Validator

Build a PasswordValidator class that:

  • Validates password strength (length, complexity)
  • Uses properties for different validation rules
  • Provides feedback on password requirements
  • Tracks validation attempts

Exercise 3: Library Book System

Create a LibraryBook class with:

  • Encapsulated borrowing status
  • Methods to borrow/return with validation
  • Property for due date calculation
  • Property for overdue status

Exercise 4: Shopping Cart with Validation

Build a ShoppingCart class that:

  • Validates item additions (price, quantity)
  • Encapsulated item list
  • Properties for subtotal, tax, total
  • Methods to add/remove items safely

Exercise 5: User Authentication System

Create a UserAuth class that:

  • Encapsulates password hashing
  • Validates login attempts
  • Tracks failed login attempts
  • Implements account lockout after failures

Summary

Encapsulation protects and controls access to your object’s data:

Access Levels:

  • public - Direct access (e.g., self.name)
  • _protected - Convention only (e.g., self._age)
  • __private - Name mangling (e.g., self.__salary)

Property Decorators:

@property
def attribute(self):
    return self._attribute

@attribute.setter
def attribute(self, value):
    # validation logic
    self._attribute = value

Benefits:

  • Data protection and validation
  • Controlled access to internal state
  • Ability to change implementation without breaking users
  • Clear interface between public and private

Best Practices:

  • Use properties for validated attributes
  • Keep implementation details private
  • Provide clear, controlled public interface
  • Validate all inputs in setters

Next: Inheritance - reusing code from parent classes! 📈