Python Encapsulation
Table of Contents + −
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
1000Notice 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.
_balanceholds the money and is marked internal.depositis the safe door for adding money.- It first checks if
amountis zero or negative. If so, it refuses and stops. - Only a positive amount actually changes the balance.
Output
Deposited 500. New balance: 1500Deposit 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 setterprint(account.balance)
account.balance = -500 # this is checked and rejectedprint(account.balance)Here is what each part does.
@propertyturns thebalancemethod into something you read like a normal attribute.- The getter just hands back the internal
_balance. @balance.settermarks the code that runs whenever someone assigns tobalance.- The setter checks for a negative value and refuses it before touching
_balance.
Output
1000750Balance cannot be negative. Change rejected.750Look 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
_balanceif 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.setterdef balance(self, new_value): self._balance = new_value
# ✅ Good: check the value before accepting it@balance.setterdef 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, writingaccount._balance = -5000from outside skips every check. Use the safe path the class offers.
# ❌ Avoid: poking at the internal value directlyaccount._balance = -5000
# ✅ Good: go through the method that validatesaccount.deposit(500)- Naming the property and the internal attribute the same thing. The property is
balanceand the data is_balance. If both arebalance, 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
depositandwithdrawinstead 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.valuefeel 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
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
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
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
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.