Python Encapsulation & Properties

๐Ÿ Python 3.12+ ๐ŸŸข Chapter 31 of 65 ๐Ÿ“‚ Phase 7: Object-Oriented Programming ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: Encapsulation ยท Public, Protected (_), Private (__) ยท Name Mangling ยท @property Decorator ยท Getters & Setters
Master data protection and encapsulation in Python: public attributes, protected conventions with single underscore (_), private attributes with double underscore (__), name mangling internals, and Pythonic getters/setters with the @property decorator.
1What is Encapsulation? Data Hiding and Access Control

Encapsulation is the bundling of data (attributes) and the methods that operate on that data into a single unit, while restricting direct outside access to internal implementation details.

In Python, access modifiers are governed by naming conventions rather than strict language keywords (like private in Java/C++):

ModifierNaming SyntaxAccessibility / Convention
Publicself.nameAccessible freely from anywhere (inside and outside the class).
Protectedself._balanceConvention only: Signals to other developers that this is internal and should only be accessed by this class and its subclasses.
Privateself.__pinEnforced by Python: Triggers Name Mangling to prevent accidental outside access or subclass overriding.
๐Ÿ’ป Example 1: Public, Protected (_), and Private (__) Modifiers
class UserAccount:
    def __init__(self, username, email, pin):
        self.username = username      # Public (Freely accessible)
        self._email = email          # Protected (Convention: internal use)
        self.__pin = pin             # Private (Name mangled!)

    def verify_pin(self, entered_pin):
        return self.__pin == entered_pin

user = UserAccount("balaji_dev", "balaji@test.com", 1234)

# 1. Accessing Public attribute:
print("Public Username:", user.username)

# 2. Accessing Protected attribute (Works, but discouraged by convention):
print("Protected Email:", user._email)

# 3. Attempting to access Private attribute directly raises AttributeError:
try:
    print(user.__pin)
except AttributeError as err:
    print("โŒ Private Access Blocked:", err)
๐Ÿ” Why user.__pin failed:

Python actively protects private variables by rewriting their names in memory so external callers cannot accidentally read or corrupt them.

2Name Mangling Internals (_ClassName__attribute)

When Python encounters an identifier starting with two or more leading underscores (__pin), it automatically rewrites the variable name internally to _ClassName__attribute (e.g. _UserAccount__pin).

This mechanism is known as Name Mangling. Its primary purpose is not cryptography, but to prevent accidental name collisions in inheritance hierarchies when subclasses define attributes with identical names.

๐Ÿ’ป Example 2: Inspecting Name Mangling in Python Memory
# Inspecting the internal __dict__ table of the instance:
user = UserAccount("ravi_k", "ravi@test.com", 9988)

print("Instance Memory Dictionary (__dict__):")
print(user.__dict__)

# Accessing the mangled name directly (Proof of Name Mangling):
print("\nAccessing via mangled name (_UserAccount__pin):", user._UserAccount__pin)
๐Ÿ” Python Philosophy:

As Python core developers famously say, "We are all consenting adults here." Name mangling discourages unsafe access while still leaving the door open for debugging tools and serializers.

3Pythonic Getters, Setters & The @property Decorator

In languages like Java, developers are forced to write verbose boilerplate methods: getBalance() and setBalance(val). In Python, you can expose methods as if they were simple attributes using the @property decorator!

This allows you to add validation, type checking, and computed logic transparently without breaking existing code that accesses the attribute directly:

๐Ÿ’ป Example 3: Clean Data Validation with @property and @setter
class BankAccount:
    def __init__(self, owner, initial_balance):
        self.owner = owner
        self._balance = 0.0          # Protected backing field
        self.balance = initial_balance # Routes through @balance.setter for validation!

    # 1. Getter property:
    @property
    def balance(self):
        """Getter: returns the current balance."""
        return self._balance

    # 2. Setter property with validation:
    @balance.setter
    def balance(self, value):
        """Setter: enforces positive balance rule."""
        if not isinstance(value, (int, float)):
            raise TypeError("Balance must be a numeric value!")
        if value < 0:
            raise ValueError(f"Balance cannot be negative! Attempted: โ‚น{value}")
        self._balance = float(value)

    # 3. Read-Only computed property:
    @property
    def formatted_balance(self):
        return f"โ‚น{self._balance:,.2f}"

# Test the property decorator:
acc = BankAccount("Balaji", 5000)
print("Balance accessed as attribute:", acc.balance) # Calls getter!
print("Formatted:", acc.formatted_balance)

# Update balance cleanly (Calls setter validation):
acc.balance = 7500
print("Updated Balance:", acc.formatted_balance)

# Attempt invalid negative update:
try:
    acc.balance = -1000 # Triggers ValueError!
except ValueError as err:
    print("๐Ÿšซ Validation Prevented Error:", err)
๐Ÿ” Why @property is Superior:

Callers use clean syntax acc.balance = 7500 without ugly method calls like acc.setBalance(7500), while your class retains 100% control over validation.

โš ๏ธ Common Developer Pitfall: Infinite Recursion Loop in @property Setter (RecursionError)

Inside the setter "def balance(self, value):", writing "self.balance = value" calls the setter again endlessly until RecursionError crashes Python! You must assign to the private backing variable: self._balance = value.

๐Ÿ’ป Hands-on Interactive Practice Challenge

Create a Temperature class with a celsius property. Add a getter and setter with a rule that temperature cannot be below absolute zero (-273.15ยฐC).

Python 3 Practice Challenge โ–ถ Run in Compiler
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is impossible!")
        self._celsius = value

t = Temperature(25)
print(f"Temperature: {t.celsius}ยฐC")
t.celsius = 38
print(f"Updated: {t.celsius}ยฐC")
Run This Challenge in Online Python IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q Is there true private data in Python like private in Java/C++?

No. Python uses name mangling (_ClassName__var) to protect private attributes. It prevents accidental collisions and warns developers, but does not provide hard memory-level access barriers.

Q What is the @deleter decorator in Python properties?

@property_name.deleter allows you to customize what happens when someone executes "del obj.property_name", such as resetting a cached value or cleaning up related resources.

Q Can a property be read-only in Python?

Yes! Simply define the @property getter without defining a corresponding @setter. Any attempt to assign to the attribute will raise an AttributeError: property has no setter.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on Python 3.12+ runtime ยท Last updated August 2026