Course topics

By WebNest Studio

Python Tutorial

Encapsulation and Access Modifiers in Python

Encapsulation means bundling data with the methods that operate on it and hiding internal details behind a clear interface. A bank account should not let anyone set its balance to any value; changes should go through deposit() and withdraw(), which enforce the rules.

Python has no private or protected keywords. Instead it uses naming conventions — public names, _protected names and __private names with name mangling — together with properties. This lesson explains all three levels and how to design well-encapsulated classes.

Public, Protected and Private by Convention

The three levels:

  • Public — name: part of the class's interface; anyone may use it.
  • Protected — _name: "internal, please don't touch from outside"; subclasses may use it. Nothing stops access — it is a convention every Python developer respects. from module import * also skips such names.
  • Private — __name: Python name-mangles it to _ClassName__name, preventing accidental access and name clashes in subclasses. It is still reachable through the mangled name, so it is not true security.

Controlled Access with Methods and Properties

Expose behaviour, not raw data: methods validate changes, and @property lets you provide read-only or validated attributes while keeping attribute syntax (see the properties lesson). This way you can change the internal representation later without breaking code that uses the class.

Why Encapsulate

Encapsulation protects invariants (a balance never goes negative), reduces coupling (callers depend on a small interface), and makes refactoring safe. "We are all consenting adults" is the Python philosophy: conventions communicate intent, and developers respect them.

Examples

Public, protected and private attributes

Python
class Account:
    def __init__(self, owner, balance):
        self.owner = owner            # public
        self._branch = "Pune"         # protected (internal)
        self.__balance = balance      # private (name-mangled)

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self.__balance += amount

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("insufficient funds")
        self.__balance -= amount

    def balance(self):
        return self.__balance

acc = Account("Asha", 1000)
acc.deposit(500)
print(acc.owner, acc._branch, acc.balance())

try:
    print(acc.__balance)
except AttributeError as e:
    print("AttributeError:", e)

print(vars(acc))
print(acc._Account__balance)        # mangled name still exists
Output
Asha Pune 1500
AttributeError: 'Account' object has no attribute '__balance'
{'owner': 'Asha', '_branch': 'Pune', '_Account__balance': 1500}
1500

Name mangling prevents clashes in subclasses

Python
class Parent:
    def __init__(self):
        self.__secret = "parent"
        self._shared = "parent"

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__secret = "child"      # a different attribute: _Child__secret
        self._shared = "child"       # overwrites the parent's

c = Child()
print(sorted(vars(c).items()))
Output
[('_Child__secret', 'child'), ('_Parent__secret', 'parent'), ('_shared', 'child')]

Encapsulating with a read-only property and validated setter

Python
class Employee:
    def __init__(self, name, salary):
        self._name = name
        self.salary = salary              # goes through the setter

    @property
    def name(self):                       # read-only
        return self._name

    @property
    def salary(self):
        return self._salary

    @salary.setter
    def salary(self, value):
        if value < 0:
            raise ValueError("salary cannot be negative")
        self._salary = value

e = Employee("Kiran", 50000)
e.salary = 55000
print(e.name, e.salary)
for action in (lambda: setattr(e, "salary", -1), lambda: setattr(e, "name", "X")):
    try:
        action()
    except (ValueError, AttributeError) as err:
        print(type(err).__name__, "-", err)
Output
Kiran 55000
ValueError - salary cannot be negative
AttributeError - property 'name' of 'Employee' object has no setter

Common Mistakes

  • Believing __private attributes are secure; they are only name-mangled.
  • Writing Java-style get_x()/set_x() for every attribute instead of plain attributes or properties.
  • Accessing another class's _protected attributes from outside it.
  • Exposing internal mutable lists directly so callers can bypass validation.

Key Points to Remember

  • Python uses conventions: public, _protected, __private.
  • __name is name-mangled to _Class__name, avoiding clashes but not providing security.
  • Expose behaviour through methods and properties that enforce rules.
  • Encapsulation protects invariants and allows internal changes without breaking callers.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.