OOPBeginner 12 min Lesson 4 of 9

Encapsulation — Protecting What Matters

Expose a small, stable surface and keep internals private, so you can change how something works without breaking everyone who uses it.

OOP · Lesson 4 of 9
0/9 done(0%)

What is it? #

Encapsulation means keeping the internals of a class private and exposing only what callers actually need.

The benefit is freedom to change. If nobody outside the class touches its internal list, you can swap that list for a dictionary later and nothing breaks.

The second benefit is protecting rules. If the only way to change a balance is through deposit and withdraw, those rules cannot be skipped.

Python uses convention rather than enforcement. A leading underscore means internal. It is not a lock, but in practice teams respect it — and tooling warns about it.

Think of it like this #

An ATM gives you a keypad and a slot. Inside there is a cash cassette, a counter and a network connection, and you cannot reach any of it.

That is not because the bank is secretive. It is because if customers could reach in, no rule about balances could be trusted, and the bank could never change the mechanism inside.

Simple example #

A bank account must never go negative and must keep a record of every transaction. Exposing the balance as a plain writable attribute would make both guarantees impossible.

Code #

PYTHON
from datetime import datetime


class Account:
    def __init__(self, owner: str, opening_balance: float = 0.0):
        if opening_balance < 0:
            raise ValueError("opening balance cannot be negative")
        self.owner = owner
        self._balance = round(opening_balance, 2)     # internal
        self._transactions: list[tuple] = []          # internal

    @property
    def balance(self) -> float:
        """Read-only from outside: changing it must go through a method."""
        return self._balance

    @property
    def transactions(self) -> tuple:
        return tuple(self._transactions)              # a copy, not the real list

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance = round(self._balance + amount, 2)
        self._record("deposit", amount)

    def withdraw(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("withdrawal must be positive")
        if amount > self._balance:
            raise ValueError(f"insufficient funds: balance is {self._balance}")
        self._balance = round(self._balance - amount, 2)
        self._record("withdraw", amount)

    def _record(self, kind: str, amount: float) -> None:   # internal helper
        self._transactions.append((datetime.now(), kind, amount))


account = Account("Ravi", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance)              # 1300.0
print(len(account.transactions))    # 2

# account.balance = 999999          # AttributeError: no setter
# account.transactions.append(...)  # affects only the copy, not the real history

try:
    account.withdraw(99999)
except ValueError as exc:
    print(exc)                      # insufficient funds: balance is 1300.0

How it works #

_balance and _transactions carry a leading underscore, marking them internal. Every Python developer reads that as "do not touch from outside".

balance is a read-only property. Callers can read it naturally, but assigning to it raises AttributeError because no setter is defined. The only way the balance changes is through deposit and withdraw, which is where the rules live.

transactions returns a tuple built from the internal list. That matters: returning self._transactions directly would hand out the real list, and a caller could append or clear it. Returning a copy keeps the history trustworthy.

_record is an internal helper. It is part of how the class works, not part of what it offers, so it is marked internal too.

The rules — positive amounts, sufficient funds — live inside the methods. There is no path around them, which means no caller needs to remember to check.

The payoff is changeability. If you later store transactions in a database instead of a list, only the inside of this class changes. Every caller using .balance, .deposit() and .withdraw() keeps working.

Real-world use #

Encapsulation is why library upgrades are survivable. The public API stays stable while internals are rewritten. Code that reached into internals is the code that breaks on every upgrade.

In application code, the common example is a service class hiding which API or database it talks to. Callers ask for a user; whether it came from cache, a database or an HTTP call is nobody else's business.

Returning copies of internal collections is a small habit with real value. Handing out the live list is the reason "something is modifying my data and I cannot find where" bugs happen.

The counter-advice matters too: do not wrap every attribute in a property out of habit. A plain public attribute is fine until there is an actual rule to enforce. Add the property when the rule appears.

Common mistakes #

  • Returning the internal list directly, letting callers modify it.
  • Adding getters and setters for everything, adding noise without protection.
  • Reaching into another object’s underscore attributes because it is quicker.
  • Treating the underscore as real security. It prevents accidents, not determined access.
  • Exposing a setter that skips validation, defeating the point of the property.

Practice #

Write an Inventory class holding a private dictionary of SKU to quantity. Expose add(sku, qty), remove(sku, qty) that refuses to go below zero, a read-only total_items property, and an items() method returning a copy. Then prove from outside that the internal dictionary cannot be corrupted.

Quick quiz

  1. 1. What is the main practical benefit of encapsulation?

  2. 2. What does a single leading underscore mean in Python?

  3. 3. Why return a copy of an internal list?

  4. 4. What happens if a property has no setter?

  5. 5. When should you add a property instead of a plain attribute?

Summary

  • Encapsulation exposes a small public surface and hides the rest.
  • Rules enforced inside methods cannot be bypassed by callers.
  • Return copies of internal collections, not the originals.
  • Underscores are a convention; the discipline comes from the team.
  • Add properties when a rule appears, not on principle.