Skip to main content

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
Simple Definition: Encapsulation = Wrapping data + methods together and controlling access to them.

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)

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)

class BankAccount:
    def __init__(self, owner_name):
        self.owner = owner_name
        self.__balance = 0  # Private! (notice the __)
    
    # Controlled way to add money
    def deposit(self, amount):
        if amount > 0:
            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:
            self.__balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.__balance}")
        else:
            print("Invalid amount or insufficient funds!")
    
    # Safe way to check balance
    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:
TypeSymbolWho Can Access?Real-World Example
PublicnameEveryoneYour name on your shirt - everyone can see
Protected_nameClass + ChildrenFamily secrets - only family knows
Private__nameOnly this classYour diary with a lock - only YOU can read
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:
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:
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

Data Protection

Prevent invalid or dangerous changes to your data

Easy Maintenance

Change internal code without breaking other parts

Fewer Bugs

Controlled access means fewer places for bugs to hide

Clear Interface

Users know exactly how to interact with your object

Practice Exercise

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

Next Up: Inheritance

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

Continue to Inheritance →

Learn how child objects can inherit from parent objects - like how a Cat and Dog both inherit from Animal!