Introduction
Encapsulation is the practice of bundling data with the methods that operate on it, and restricting direct access to that data from outside the class. The goal is to protect an object's internal state from being set into an invalid or inconsistent configuration, and to let a class change its internal implementation later without breaking code that depends on it. Unlike some languages, Python does not enforce strict access control with keywords like private; instead it relies on naming conventions and the property mechanism.
Cricket analogy: A team's dressing room keeps team strategy and player fitness data internal, letting only the coach and captain change tactics, so opponents and media can't corrupt the game plan mid-match.
Explanation
Python has three levels of attribute visibility, all convention-based rather than enforced by the interpreter (with one partial exception). A name with no leading underscore, like self.balance, is public — freely accessible from anywhere. A name with a single leading underscore, like self._balance, is protected by convention: it signals 'this is internal, please don't touch it directly from outside the class,' but Python does not stop external code from accessing it. A name with a double leading underscore (and no more than one trailing underscore), like self.__balance, triggers name mangling: Python internally rewrites it to _ClassName__balance, which makes accidental access or accidental overriding by subclasses much less likely, though it is still technically reachable if you know the mangled name. To expose controlled access to a protected or private attribute, Python provides the @property decorator for a getter and the matching @x.setter decorator for a setter, letting a class validate values on write while still offering attribute-like syntax (obj.balance = 100) to callers.
Cricket analogy: A domestic player is openly listed on the scorecard (public), a reserve player is 'protected' by convention as bench-only, and a banned player's name is mangled in records so accidental selection is unlikely but still traceable.
Example
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # public
self._balance = balance # protected by convention
self.__pin = "0000" # name-mangled to _BankAccount__pin
@property
def balance(self):
"""Getter: read-only view of the protected attribute."""
return self._balance
@balance.setter
def balance(self, amount):
"""Setter: validates before mutating internal state."""
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount # goes through the setter's validation
account = BankAccount("Priya", 100)
account.deposit(50)
print(account.balance) # 150 (via the getter)
try:
account.balance = -10 # blocked by the setter's validation
except ValueError as e:
print(f"Rejected: {e}")
print(account._BankAccount__pin) # name-mangled, still technically reachableAnalysis
owner is public because there is no reason to restrict access to it. _balance is protected by convention: it stores the real data, but external code is expected to go through the balance property rather than touching _balance directly. The @property getter lets account.balance be read with plain attribute syntax while actually calling a method behind the scenes. The @balance.setter intercepts every assignment to account.balance, including the one inside deposit(), and raises ValueError if the new value would be invalid — this is the essence of encapsulation: the class, not the caller, enforces its own invariants. __pin demonstrates name mangling: Python rewrites it to _BankAccount__pin internally, which is why account.__pin from outside the class would raise AttributeError, yet the mangled name account._BankAccount__pin still works, showing that double-underscore attributes are obscured, not truly private.
Cricket analogy: A player's public jersey number is freely known, but the team's real batting order strategy is only revealed through the official team sheet, which the captain validates before submission, rejecting any lineup that breaks selection rules.
Key Takeaways
- Single underscore (
_x) signals 'protected by convention' — accessible but not meant to be touched externally. - Double underscore (
__x) triggers name mangling to_ClassName__x, discouraging accidental access or override. @propertyand@x.setterlet a class expose controlled, validated access while keeping attribute-like syntax.- Python does not enforce true access control the way languages like Java do — it relies on convention and discipline.
Practice what you learned
1. What does a single leading underscore (e.g., `_balance`) signal in Python?
2. What does Python do with an attribute named `__pin` inside a class `BankAccount`?
3. In the example, what is the purpose of the `@balance.setter` method?
4. Why does `account.deposit(50)` go through validation even though `deposit` sets `self.balance` directly?
Was this page helpful?
You May Also Like
OOP Core Concepts
An introduction to the four pillars of object-oriented programming: encapsulation, abstraction, inheritance, and polymorphism.
Classes and Objects
How classes act as blueprints for objects, and the difference between class attributes and instance attributes.
Inheritance
How single inheritance, `super().__init__()`, and method overriding let subclasses reuse and specialize parent class behavior.
SOLID Principles Overview
A guided tour of the five SOLID design principles that make object-oriented code easier to maintain and extend.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringTesting & TDD Study Notes
Software Testing · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics