Encapsulation keeps an object’s data and the code that manages that data together. In Python, it is less about absolute access control and more about designing a clear, safe interface for changing object state.
In OSPython.014: Polymorphism Basics, we saw different objects respond to the same method name in different ways. Encapsulation focuses on a different question: how should outside code interact with the data inside one object?
Start with a simple class
class Miner:
def __init__(self, model, temperature):
self.model = model
self.temperature = temperature
miner = Miner("S21", 72)
print(miner.model)
print(miner.temperature)
Both attributes are public, so code outside the class can read or replace them directly. That is simple, but it also means nothing stops another part of the program from assigning an impossible value such as -500 degrees.
Video 1: Public, protected, and private members
Python uses naming conventions
Python does not enforce access modifiers exactly like languages such as Java or C++. Instead, Python relies heavily on naming conventions.
name— public attribute; normal outside access is expected._name— protected-style convention; outside code can access it, but the leading underscore says “treat this as internal.”__name— private-style name that triggers name mangling inside the class.
class Wallet:
def __init__(self, owner, balance):
self.owner = owner
self._network = "Bitcoin"
self.__balance = balance
wallet = Wallet("Alex", 2500)
print(wallet.owner)
print(wallet._network)
owner is public. _network is still accessible, but its underscore signals that callers should avoid depending on it directly. The double-underscore attribute behaves differently.
Double underscores and name mangling
When an attribute begins with two underscores, Python rewrites its internal name. This is called name mangling.
class Wallet:
def __init__(self, balance):
self.__balance = balance
wallet = Wallet(2500)
# This raises AttributeError:
# print(wallet.__balance)
# Python internally mangles the name:
print(wallet._Wallet__balance)
Name mangling makes accidental access harder, but it is not a security boundary. A determined caller can still reach the mangled attribute. The main goal is API design: clearly communicate which values outside code should use directly and which values the class should manage itself.
Video 2: Encapsulation with @property
Use a property to control updates
A property lets callers use normal attribute syntax while the class runs logic behind the scenes.
class Miner:
def __init__(self, model, temperature):
self.model = model
self._temperature = temperature
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, value):
if value < -40 or value > 120:
raise ValueError("Temperature is outside the allowed range")
self._temperature = value
miner = Miner("S21", 72)
print(miner.temperature)
miner.temperature = 78
print(miner.temperature)
Outside code still writes miner.temperature = 78, but the setter gets a chance to validate the new value first. That is a practical form of encapsulation: the class protects its own rules.
Why properties are useful
- Validate values before saving them.
- Prevent invalid object state.
- Expose a clean interface without forcing callers to use
get_...andset_...methods everywhere. - Change the internal implementation later without changing the code that uses the class.
A game example
class Player:
def __init__(self, health):
self._health = 0
self.health = health
@property
def health(self):
return self._health
@health.setter
def health(self, value):
if value < 0:
self._health = 0
elif value > 100:
self._health = 100
else:
self._health = value
player = Player(100)
player.health = 135
print(player.health) # 100
player.health = -10
print(player.health) # 0
The class owns the health rules. Other parts of the game can request a new health value, but the Player object decides what values are valid.
Video 3: Getters, setters, and the property decorator
Encapsulation is not “make everything private”
Good encapsulation is selective. A class should expose the operations that callers actually need while keeping implementation details behind a stable interface.
class MiningRig:
def __init__(self, name):
self.name = name
self._online = False
def start(self):
self._online = True
def stop(self):
self._online = False
@property
def online(self):
return self._online
rig = MiningRig("Rack-A1")
rig.start()
print(rig.name)
print(rig.online)
Callers can start and stop the rig through methods and read its current status through a property. They do not need to know how the internal state is stored.
Common beginner mistakes
- Assuming
_nameis truly inaccessible from outside the class. - Assuming
__namecreates strong security. It mainly triggers name mangling. - Writing getters and setters that add no validation or behavior when a normal public attribute would be simpler.
- Changing internal attributes directly from unrelated code instead of using the class interface.
- Forgetting to use a backing attribute such as
_temperatureinside a property setter, which can accidentally cause recursion.
Practice
- Create a
Batteryclass with a protected-style_chargeattribute. - Create a
chargeproperty that returns the current percentage. - Add a setter that accepts only values from 0 through 100.
- Raise
ValueErrorfor values outside that range. - Create one object, set valid and invalid values, and observe the results.
Key takeaway
Encapsulation gives an object responsibility for protecting its own state and presenting a clean interface to the rest of the program. Python usually does this with naming conventions, name mangling, methods, and especially properties. Review OSPython.012: Classes and Objects, OSPython.013: Inheritance Basics, and OSPython.014: Polymorphism Basics if you want the full OOP progression in order.

Leave a comment