> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Encapsulation

> Learn to protect your data like a treasure chest with a lock

## What is Encapsulation?

Imagine you have a **piggy bank**. You put money IN through a small slot, and you can check how much is inside, but you can't just reach in and grab money whenever you want. The piggy bank **protects** your money.

**Encapsulation** works the same way:

* Put data (like money) inside an object
* Control HOW that data can be accessed or changed
* Protect the data from being messed up accidentally

<Tip>
  **Simple Definition**: Encapsulation = **Wrapping data + methods together** and **controlling access** to them.
</Tip>

**Another way to think about it:** Encapsulation is like a vending machine. You interact through a defined set of buttons (the public interface). You insert coins, press a button, and get a drink. You cannot reach inside the machine, rearrange its inventory, or change the pricing logic directly. The machine's internal mechanics are hidden, and the only way to interact is through the controlled interface it exposes. This constraint is not a limitation -- it is what prevents chaos.

***

## Real-World Example: Bank Account

Think about your bank account:

* You can **deposit** money
* You can **withdraw** money (if you have enough)
* You can **check** your balance
* You CANNOT directly change your balance to a million dollars

The bank **protects** your balance and only lets you change it through proper methods!

### Bad Code (No Protection)

```python theme={null}
class BankAccount:
    def __init__(self):
        self.balance = 0  # Anyone can change this directly!

# The problem:
account = BankAccount()
account.balance = 1000000  # Hacked! Changed directly!
account.balance = -500     # Negative balance? That's not right!
```

### Good Code (With Encapsulation)

```python theme={null}
class BankAccount:
    def __init__(self, owner_name):
        self.owner = owner_name
        self.__balance = 0  # Private! (notice the __)
        # DESIGN REASONING: Balance is private because it should only
        # change through validated operations. This is the core idea --
        # data and the rules that govern it live together.
    
    # Controlled way to add money
    def deposit(self, amount):
        if amount > 0:  # Validation: the "gate" that protects data integrity
            self.__balance += amount
            print(f"Deposited ${amount}. New balance: ${self.__balance}")
        else:
            print("Amount must be positive!")
    
    # Controlled way to take money
    def withdraw(self, amount):
        if amount > 0 and amount <= self.__balance:  # Business rule enforced here
            self.__balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.__balance}")
        else:
            print("Invalid amount or insufficient funds!")
    
    # Safe way to check balance (read-only access)
    def get_balance(self):
        return self.__balance

# Now try to hack it!
account = BankAccount("Alice")
account.deposit(500)           # Works! Balance is 500
account.withdraw(100)          # Works! Balance is 400
account.withdraw(1000)         # Blocked! Not enough money
print(account.get_balance())   # Shows 400

# Try to hack directly?
account.__balance = 1000000    # Seems to work...
print(account.get_balance())   # Still 400! Python protected it!
```

***

## Public, Private, and Protected

In OOP, we have three levels of access:

| Type          | Symbol   | Who Can Access?  | Real-World Example                         |
| ------------- | -------- | ---------------- | ------------------------------------------ |
| **Public**    | `name`   | Everyone         | Your name on your shirt - everyone can see |
| **Protected** | `_name`  | Class + Children | Family secrets - only family knows         |
| **Private**   | `__name` | Only this class  | Your diary with a lock - only YOU can read |

```python theme={null}
class Student:
    def __init__(self, name, grade, diary_secret):
        self.name = name              # Public: Everyone can see
        self._grade = grade           # Protected: For class and children
        self.__diary = diary_secret   # Private: Only for this class
    
    def read_diary(self, password):
        if password == "secret123":
            return self.__diary
            return "Wrong password!"

student = Student("Bob", "A", "I like pizza")
print(student.name)           # Works: Bob
print(student._grade)         # Works but shouldn't access: A
print(student.__diary)        # Error! Can't access private
print(student.read_diary("secret123"))  # Works: I like pizza
```

***

## Fun Example: Video Game Character

Let's create a game character with proper encapsulation:

```python theme={null}
class GameCharacter:
    def __init__(self, name, character_class):
        # Public - everyone needs to see
        self.name = name
        self.character_class = character_class
        
        # Private - protect from cheating!
        self.__health = 100
        self.__max_health = 100
        self.__level = 1
        self.__experience = 0
        self.__gold = 50
    
    # Take damage (controlled)
    def take_damage(self, damage):
        if damage < 0:
            print("Nice try, cheater!")
            return
        
        self.__health -= damage
        if self.__health <= 0:
            self.__health = 0
            print(f"{self.name} has been defeated!")
        else:
            print(f"{self.name} took {damage} damage! Health: {self.__health}")
    
    # Heal (controlled)
    def heal(self, amount):
        if amount < 0:
            return
        
        self.__health = min(self.__health + amount, self.__max_health)
        print(f"{self.name} healed! Health: {self.__health}")
    
    # Gain XP (controlled leveling)
    def gain_experience(self, xp):
        if xp < 0:
            return
        
        self.__experience += xp
        print(f"+{xp} XP!")
        
        # Level up every 100 XP
        while self.__experience >= 100:
            self.__experience -= 100
            self.__level += 1
            self.__max_health += 20
            self.__health = self.__max_health
            print(f"LEVEL UP! Now level {self.__level}!")
    
    # Gold management
    def earn_gold(self, amount):
        if amount > 0:
            self.__gold += amount
            print(f"+{amount} gold! Total: {self.__gold}")
    
    def spend_gold(self, amount):
        if amount <= self.__gold:
            self.__gold -= amount
            print(f"Spent {amount} gold. Remaining: {self.__gold}")
            return True
        print("Not enough gold!")
        return False
    
    # Show stats (read-only access)
    def show_stats(self):
        print(f"""
        ╔════════════════════════════╗
        ║  {self.name} the {self.character_class}
        ╠════════════════════════════╣
        ║  Health: {self.__health}/{self.__max_health}
        ║  Level:  {self.__level}
        ║  XP:     {self.__experience}/100
        ║  Gold:   {self.__gold}
        ╚════════════════════════════╝
        """)

# Let's play!
hero = GameCharacter("Luna", "Mage")
hero.show_stats()

hero.take_damage(30)
hero.heal(10)
hero.gain_experience(150)  # Should level up!
hero.earn_gold(100)
hero.show_stats()

# Try to cheat?
hero.__health = 9999      # Won't work! 
hero.__gold = 999999      # Won't work!
hero.show_stats()         # Still the same real values!
```

***

## Getters and Setters (Properties)

Sometimes we want to:

* **Read** a private value (Getter)
* **Change** a private value with rules (Setter)

Python has a beautiful way to do this with `@property`:

```python theme={null}
class Temperature:
    def __init__(self):
        self.__celsius = 0
    
    # GETTER - Read the temperature
    @property
    def celsius(self):
        return self.__celsius
    
    # SETTER - Change temperature with validation
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:  # Absolute zero
            print("Temperature can't be below absolute zero!")
        else:
            self.__celsius = value
    
    # Bonus: Auto-calculate Fahrenheit!
    @property
    def fahrenheit(self):
        return (self.__celsius * 9/5) + 32

# Use it like a normal attribute!
temp = Temperature()
temp.celsius = 25           # Uses setter (validated!)
print(temp.celsius)         # Uses getter: 25
print(temp.fahrenheit)      # Auto-calculated: 77.0

temp.celsius = -300         # Blocked! Below absolute zero
print(temp.celsius)         # Still 25
```

***

## Benefits of Encapsulation

<CardGroup cols={2}>
  <Card title="Data Protection" icon="shield">
    Prevent invalid or dangerous changes to your data
  </Card>

  <Card title="Easy Maintenance" icon="wrench">
    Change internal code without breaking other parts
  </Card>

  <Card title="Fewer Bugs" icon="bug">
    Controlled access means fewer places for bugs to hide
  </Card>

  <Card title="Clear Interface" icon="book">
    Users know exactly how to interact with your object
  </Card>
</CardGroup>

***

## Practice Exercise

<Accordion title="Challenge: Create a Password Manager" icon="lock">
  Try creating a PasswordManager class that:

  1. Stores passwords privately (you can't see them directly)
  2. Has a method to add a password for a website
  3. Has a method to get a password (with master password check)
  4. Never allows password to be shorter than 8 characters

  ```python theme={null}
  class PasswordManager:
      def __init__(self, master_password):
          self.__master = master_password
          self.__passwords = {}  # {website: password}
      
      def add_password(self, website, password):
          # TODO: Check if password is at least 8 characters
          # TODO: Store in __passwords dictionary
          pass
      
      def get_password(self, website, master_password):
          # TODO: Check master password first
          # TODO: Return the password if correct, else "Access Denied"
          pass

  # Test it:
  pm = PasswordManager("mymaster123")
  pm.add_password("google.com", "securepass123")
  print(pm.get_password("google.com", "mymaster123"))  # Should work
  print(pm.get_password("google.com", "wrongpass"))    # Should fail
  ```

  <details>
    <summary>Click for Solution</summary>

    ```python theme={null}
    class PasswordManager:
        def __init__(self, master_password):
            self.__master = master_password
            self.__passwords = {}
        
        def add_password(self, website, password):
            if len(password) < 8:
                print("Password must be at least 8 characters!")
                return False
            self.__passwords[website] = password
            print(f"Password saved for {website}")
            return True
        
        def get_password(self, website, master_password):
            if master_password != self.__master:
                return "Access Denied - Wrong master password!"
            
            if website in self.__passwords:
                return f"Password for {website}: {self.__passwords[website]}"
            return "No password found for this website"
    ```
  </details>
</Accordion>

***

## Why Encapsulation Matters in Production

In real systems, encapsulation is what prevents one team's code from accidentally corrupting another team's data. Consider a large e-commerce platform: the Pricing team exposes a `calculate_price(product_id, quantity)` method but hides the discount rules, tax calculations, and supplier cost data behind that interface. If the Checkout team could directly modify the price field, a bug in checkout code could silently create orders with negative prices. Encapsulation makes these impossible states unrepresentable.

**A senior engineer would say:** "Encapsulation is not about privacy for privacy's sake -- it is about defining a contract. The public methods are your promises to the rest of the codebase. The private data is your freedom to change implementation without breaking those promises."

***

## Interview Insight

<Info>
  **Common interview question:** "What is encapsulation and why does it matter?" Weak candidates recite the definition. Strong candidates explain *the design benefit*: encapsulation creates a boundary between what can change (implementation) and what must stay stable (interface). Mention that encapsulation enables refactoring -- you can rewrite the internal logic of a class without touching any calling code, as long as the public interface stays the same. Bonus points if you connect it to the "information hiding" principle from David Parnas (1972), which is the academic origin of encapsulation.
</Info>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="An interviewer asks: 'Python has no real private fields. Is encapsulation in Python pointless compared to Java?'">
    **Strong Answer:**

    * Python's double-underscore name mangling is convention-based, not enforced by the runtime like Java's private keyword. Anyone can access account.\_BankAccount\_\_balance if they know the convention. But this does not make encapsulation pointless.
    * Encapsulation is about communicating intent and protecting invariants during normal development. The double underscore tells every developer: "This is internal. Do not depend on it." All mutations go through controlled methods where validation, logging, and business rules are enforced.
    * In practice, what protects your code is code review, linting rules, and tests -- not language enforcement. Python's approach trades compile-time enforcement for developer productivity and flexibility. Java's approach trades flexibility for stronger guarantees.
    * A strong answer connects this to the real-world benefit: if every modification to balance goes through deposit() and withdraw(), your audit trail is complete, your invariants are maintained, and refactoring the internal representation (say, from a float to a Decimal) affects zero external code.

    **Follow-up: The get\_statement() method returns self.\_\_transactions.copy(). Why not return the list directly?**

    Returning the reference directly would let external code call .append() or .clear() on the internal list, mutating the object's state without going through any controlled method. This is a "reference leak" -- it breaks encapsulation even though the field is private. Returning a copy ensures the caller gets a read-only snapshot.
  </Accordion>

  <Accordion title="Design a thread-safe BankAccount. What changes to encapsulation are needed for concurrent deposits and withdrawals?">
    **Strong Answer:**

    * The current class is not thread-safe because deposit() and withdraw() perform read-modify-write on \_\_balance without synchronization. Two concurrent deposits of 100 could result in only 100 being added instead of 200.
    * I would add a threading.Lock as a private attribute and acquire it in every method that reads or modifies \_\_balance. The lock is part of the encapsulated implementation -- callers never see it.
    * Encapsulation is even more critical in concurrent code. If \_\_balance were public, any thread could modify it without acquiring the lock, silently creating race conditions. Private fields plus controlled methods are the only way to guarantee synchronized access.
    * For transfers between two accounts, you must lock both, which risks deadlock. The solution is consistent lock ordering -- always lock the lower account ID first.

    **Follow-up: Would you use @property or explicit get/set methods for the thread-safe version?**

    I would use explicit methods like deposit() and withdraw() for state-changing operations because they make the locking and side effects visible. For read-only access, @property (balance) is fine since it only reads inside the lock. The rule: if it looks like field access, use @property. If it is an action with side effects, use a method.
  </Accordion>

  <Accordion title="How does encapsulation relate to the Single Responsibility Principle? Can a class have good encapsulation but still be poorly designed?">
    **Strong Answer:**

    * Absolutely. A God class with all private fields and controlled methods has perfect encapsulation but terrible design. If UserManager has private fields for database connections, email configs, and report templates, and controlled methods for user CRUD, email sending, and report generation -- the encapsulation is fine but SRP is violated.
    * Encapsulation hides the "how" within a class; SRP ensures the class has a focused "what." They are complementary. A class with a single responsibility but all public fields is fragile because external code can put it in an invalid state. A class with good encapsulation but many responsibilities is hard to test and maintain because changes to one concern risk breaking another.
    * In interviews, I would say: "Encapsulation protects the internal consistency of a class. SRP ensures the class only needs to be consistent about one thing. You need both."

    **Follow-up: Give an example where breaking encapsulation is the pragmatic choice.**

    In Python, test code sometimes accesses private fields to verify internal state (assert account.\_BankAccount\_\_balance == 500). This is a deliberate, controlled violation for testing purposes. The alternative -- adding a public method solely for testing -- pollutes the production API. Most teams accept this trade-off and enforce "no private access in production code" through linting rules, while allowing it in test files.
  </Accordion>
</AccordionGroup>

***

## Next Up: Inheritance

Now that you know how to protect your data, let's learn how to **share abilities between related objects**!

<Card title="Continue to Inheritance →" icon="sitemap" href="/lld/oop/inheritance">
  Learn how child objects can inherit from parent objects - like how a Cat and Dog both inherit from Animal!
</Card>
