Concepts / Object-oriented foundations / Encapsulation

Encapsulation

Hiding data behind methods so an object's rules cannot be broken from outside.

01The intuition

The problem it solves

Imagine a small program for a school canteen card. Each card has a balance, and the rule of the canteen is simple: a balance can never go below zero. The obvious way to write it is a class with a public number and code all over the program that changes that number directly.

class CanteenCard:
    def __init__(self):
        self.balance = 0

card = CanteenCard()
card.balance += 50          # top-up at the office
card.balance -= 30          # lunch
card.balance -= 30          # another lunch, balance is now -10

Nothing stopped the second lunch. The rule "never below zero" lives in the head of the programmer, not in the program. Now imagine 40 places in the code that touch balance. To enforce the rule you must find all 40, and the next person who adds a 41st will forget. A bug like this hides for weeks, because every single line looks reasonable on its own.

Encapsulation is the idea that the data and the rules about that data live in the same place, behind a small set of methods, and nobody else is allowed to touch the data directly.

Build it one piece at a time

Step 1: make the data private by convention. In Python a leading underscore says "do not touch this from outside". Nothing else changes yet, but every direct write now looks wrong to a reader.

class CanteenCard:
    def __init__(self):
        self._balance = 0

Step 2: add the operations the canteen actually performs. The outside world does not want to "set the balance"; it wants to top up and to pay. Give it exactly those two verbs.

class CanteenCard:
    def __init__(self):
        self._balance = 0

    def top_up(self, amount: int) -> None:
        self._balance += amount

    def pay(self, amount: int) -> None:
        self._balance -= amount

Step 3: put the rule inside the method that could break it. There is now only one line in the whole program that lowers the balance, so the rule has only one place to live.

class InsufficientBalance(Exception):
    pass

class CanteenCard:
    def __init__(self):
        self._balance = 0

    def top_up(self, amount: int) -> None:
        if amount <= 0:
            raise ValueError("top-up must be positive")
        self._balance += amount

    def pay(self, amount: int) -> None:
        if amount > self._balance:
            raise InsufficientBalance(f"need {amount}, have {self._balance}")
        self._balance -= amount

    @property
    def balance(self) -> int:
        return self._balance

Step 4: expose reading, not writing. The balance property lets anyone look. Nobody can assign to it. The 40 call sites now read card.pay(30), which is shorter and cannot go wrong.

The state after each step, as a table of who may do what:

StepWho can read balanceWho can change balanceWhere the rule lives
Starteveryoneeveryonenowhere
1everyone (by convention)everyone (by convention)nowhere
2nobody outsidetwo methodsnowhere yet
3 and 4everyone, read-onlytwo methodsinside pay and top_up

Why it works

The principle is: a class owns an invariant, a sentence that must be true before and after every public method. Here the invariant is "balance is at least zero". Because the only code that can change _balance is inside the class, you can check the invariant by reading one file instead of the whole program. A method either keeps the invariant or raises an error before changing anything.

It also means the inside can change without the outside noticing. If the canteen later wants a list of transactions instead of a single number, pay and top_up append to a list and balance sums it. The 40 call sites do not change at all.

Where it breaks: when a method hands out a mutable internal object. If the card had self._history as a list and a method returned that list, a caller could append a fake refund. Return a copy, a tuple, or a read-only view. Interviewers test for this by asking "what if I call get_history().append(...)".

The trade-off you must say out loud

Every rule costs a method, and a class with thirty tiny getters and setters is just a public struct wearing a disguise. Encapsulate the data that has rules; leave plain data plain. A Point with x and y has no rule to protect, and a dataclass is the honest design. The follow-up question is usually "how would you test this", and the answer is that the invariant gives you the tests: one that pays within balance, one that pays over it and expects the exception.

Where it shows up in the 100 Google problems

  • Design an LRU cache: the linked list and the map must always agree, and only get and put are allowed to move nodes.
  • Design a key-value store with TTL: "never return an expired value" is enforced inside get, not by every caller checking the clock.
  • Design the Snake game: the body deque and the set of occupied cells change together inside move, so the collision check can trust the set.
  • Design a leaderboard class: scores enter only through add_score, which keeps the heap and the score map in step.
  • Design tic-tac-toe: the row, column and diagonal counters are updated in one place, which is what makes the O(1) win check correct.

02Used in these problems