Python Encapsulation

In the last lesson you learned Polymorphism, where one action behaves the right way for different kinds of objects. Now we look at a different idea. It is about keeping objects safe. How do you stop other code from reaching inside an object and breaking it? That is what encapsulation is about. This lesson walks you through it step by step.

🤔 Why Encapsulation?

Picture a bank account object. It has a balance. Now imagine any line of code anywhere can reach in and set that balance to anything it wants. Someone writes account.balance = -5000 by mistake. Nothing stops them. Now your account holds a negative balance that should never have been possible.

That is the pain. When an object’s data is wide open, any code can put it into a broken state. And then bugs show up far away from where the real mistake happened.

The fix is encapsulation. You keep the data tucked inside the object. Then you control how it gets read and changed. So the object can check every change and refuse the bad ones.

🧩 What Is Encapsulation?

Encapsulation means two things working together.

  • The object keeps its own data inside itself.
  • The object decides how that data can be read and changed.

Think of an ATM. You cannot open the machine and grab the cash directly. You go through the screen and the buttons. The machine checks your card and your PIN first. Then it gives you money only if everything is fine. The cash is protected. Every action goes through a safe path.

A class works the same way. You hide the raw data. Then you offer safe methods or properties to work with it. The outside world uses those safe paths, not the raw data.

🔒 The Leading-Underscore Convention

Python does not have a strict “private” keyword like some other languages. Instead it uses a naming habit that every Python developer understands. You put one underscore in front of an attribute name. That says “this is internal, please do not touch it from outside.”

Here is a small account class that marks its balance as internal.

class Account:
def __init__(self, balance):
self._balance = balance # the leading _ means "internal, leave it alone"
account = Account(1000)
print(account._balance)

The _balance name has one underscore in front. That is a signal to other programmers.

Output

1000

Notice that Python still lets you read account._balance. The underscore does not lock anything. It is a polite agreement, not a wall. It tells everyone “treat this as private and go through the proper methods instead.”

Note

One underscore like _balance is a hint that says “internal.” It is the most common way Python developers mark an attribute as private.

🚪 Controlling Changes With a Method

Marking data as internal is only half the job. The real value of encapsulation comes from validation. Validation means checking a value before you accept it. So a bad value never gets in.

Let us give the account a proper way to add money. The method checks the amount first. Then it updates the balance only if the amount makes sense.

class Account:
def __init__(self, balance):
self._balance = balance
def deposit(self, amount):
if amount <= 0:
print("Deposit must be a positive amount.")
return
self._balance += amount
print(f"Deposited {amount}. New balance: {self._balance}")
account = Account(1000)
account.deposit(500)
account.deposit(-200)

Read it from the top.

  • _balance holds the money and is marked internal.
  • deposit is the safe door for adding money.
  • It first checks if amount is zero or negative. If so, it refuses and stops.
  • Only a positive amount actually changes the balance.

Output

Deposited 500. New balance: 1500
Deposit must be a positive amount.

See what happened? The 500 deposit went through. The -200 deposit was caught and refused. The balance never went into a bad state. That is encapsulation doing its job. Every change goes through one checked path.

🏷️ A Cleaner Way: Properties With a Getter and Setter

The method approach works well. But sometimes you want the simple feel of reading and writing an attribute, while still keeping the safety checks. Python gives you properties for exactly this. A property lets a plain-looking line like account.balance = 500 quietly run your checking code.

You build a property with two pieces. A getter runs when someone reads the value. A setter runs when someone tries to change it. The setter is where you put your validation.

Here is the account again, this time with a balance property.

class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, new_value):
if new_value < 0:
print("Balance cannot be negative. Change rejected.")
return
self._balance = new_value
account = Account(1000)
print(account.balance) # reading goes through the getter
account.balance = 750 # setting goes through the setter
print(account.balance)
account.balance = -500 # this is checked and rejected
print(account.balance)

Here is what each part does.

  • @property turns the balance method into something you read like a normal attribute.
  • The getter just hands back the internal _balance.
  • @balance.setter marks the code that runs whenever someone assigns to balance.
  • The setter checks for a negative value and refuses it before touching _balance.

Output

1000
750
Balance cannot be negative. Change rejected.
750

Look at the last two lines closely. The attempt to set -500 was rejected by the setter. So when we print again, the balance is still 750. The object protected itself. And the calling code still got to use the clean account.balance syntax. That is the nice part. The safety is hidden inside, and the outside code stays simple.

📋 Internal vs Property at a Glance

Here is a quick way to keep the pieces straight.

Piece What it does
_balance The internal data, marked private with one underscore
getter (@property) Runs when you read the value
setter (@balance.setter) Runs when you change the value, and validates it first
method like deposit A named safe action that updates the data with its own checks

⚠️ Common Mistakes

A few things confuse people the first time. Watch for these.

  • Thinking one underscore makes data truly private. It does not. It is a convention, a polite signal. Other code can still reach _balance if it really wants to.

  • Forgetting to validate inside the setter. If your setter just stores the value with no check, you have gained nothing over a plain attribute.

# ❌ Avoid: a setter that does no checking is pointless
@balance.setter
def balance(self, new_value):
self._balance = new_value
# ✅ Good: check the value before accepting it
@balance.setter
def balance(self, new_value):
if new_value < 0:
print("Balance cannot be negative.")
return
self._balance = new_value
  • Reaching past the safe methods. Even when a class gives you deposit, writing account._balance = -5000 from outside skips every check. Use the safe path the class offers.
# ❌ Avoid: poking at the internal value directly
account._balance = -5000
# ✅ Good: go through the method that validates
account.deposit(500)
  • Naming the property and the internal attribute the same thing. The property is balance and the data is _balance. If both are balance, the setter calls itself again and again until the program crashes.

✅ Best Practices

Small habits that keep your objects safe and easy to trust.

  • Mark data that the outside should not touch with a leading underscore, like _balance. It signals your intent to every reader.
  • Put your validation in one place, the setter or the method, so every change passes the same check.
  • Offer clear, named actions like deposit and withdraw instead of letting callers edit raw values.
  • Keep the getter simple. It should just return the value, not change anything.
  • Use a property when you want the easy object.value feel but still need safety checks underneath.

🧩 What You’ve Learned

✅ Encapsulation means keeping an object’s data inside it and controlling how that data is read and changed.

✅ A leading underscore like _balance is Python’s convention for “internal, please do not touch from outside.”

✅ A method like deposit can validate a value first and refuse bad changes, so the object never reaches a broken state.

✅ A property gives you a getter and a setter, so a plain object.value = x line can quietly run your safety checks.

✅ The underscore is a polite agreement, not a hard lock, but going through safe methods keeps your objects reliable.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does a single leading underscore, like _balance, mean in Python?

    Why: One underscore is a naming convention. It signals 'internal', but Python does not actually block access to it.

  2. 2

    Why put validation inside a setter or a method instead of letting code set the value directly?

    Why: Routing changes through a validated setter or method means bad values like a negative balance get rejected before they are stored.

  3. 3

    What runs when you read account.balance if balance is defined with @property?

    Why: Reading a property calls its getter, which is the method marked with @property.

  4. 4

    What is the main risk of writing account._balance = -5000 from outside the class?

    Why: Touching the internal value directly bypasses every check the class provides, which is exactly what encapsulation is meant to prevent.

🚀 What’s Next?

You can now keep your objects’ data safe and control how it changes. Next we look at hiding the messy details entirely and showing only what a user of your class needs to see.

Abstraction

Share & Connect