Skip to content
FollowtheDamnCode

Encapsulation

Hide an object's data behind a small set of methods, so its rules can't be bypassed and its insides can change freely.

Big idea

Encapsulation means outsiders ask an object to do things; they don't reach in and change its data. That one habit buys you two things: the object's rules can't be skipped, and its internals can be rewritten without breaking anyone who uses it.

The problem it solves

Say a Thermostat stores target in Celsius, and fifteen places in the code write thermostat.target = 21 directly. Now two things go wrong:

  1. Someone writes thermostat.target = 400. Nothing objects — there's no place to put the "between 5 and 30 degrees" rule except all fifteen call sites.
  2. You decide to store tenths of a degree as an integer to avoid float drift. Every one of the fifteen call sites breaks.

When every caller depends on how the data is stored, the data's shape is frozen and its rules are scattered. Encapsulation puts a door in front of the data.

Think of it like

A vending machine. You press B4 and money goes in a slot; a can comes out. You never open the back panel to grab a can or rearrange the coin tubes, and the operator can swap the coin mechanism for a card reader without retraining a single customer. The buttons are the public interface; everything behind the panel is private.

How it works

  • Keep attributes private (in Python, by convention, a leading underscore: _target; Java and C# enforce private).
  • Offer methods that express intent — raise_by(2), set_target(21) — and validate inside them.
  • Where you need read access, expose a getter or a read-only property, not the raw field.
  • Return copies or read-only views of internal collections, so callers can't mutate them behind your back.
The public surface is small; the state stays inside

In code

A thermostat whose storage changed without callers noticing
class Thermostat:
    MIN, MAX = 5.0, 30.0

    def __init__(self, celsius=20.0):
        self._tenths = 0                 # stored as integer tenths — callers never know
        self.set_target(celsius)

    @property
    def target(self):                    # read access looks like a field...
        return self._tenths / 10

    def set_target(self, celsius):       # ...but every write goes through the rule
        if not (self.MIN <= celsius <= self.MAX):
            raise ValueError(f"{celsius} is outside {self.MIN}-{self.MAX}")
        self._tenths = round(celsius * 10)

    def raise_by(self, degrees):
        self.set_target(self.target + degrees)


t = Thermostat()
t.raise_by(1.5)
print("target:", t.target)

for attempt in (28.0, 45.0):
    try:
        t.set_target(attempt)
        print("set to", t.target)
    except ValueError as err:
        print("refused:", err)
output
target: 21.5
set to 28.0
refused: 45.0 is outside 5.0-30.0

Run and edit it in the app.

Switching from a float to integer tenths touched one class and zero callers — that's the payoff.

Why route every change to a thermostat's target through set_target() instead of letting callers assign the field?

Watch it

Leaky encapsulation to watch for

  • Getter/setter for every field. get_x()/set_x() pairs with no rules are public fields with extra typing. Ask what the caller is trying to do and offer that (ship(), not set_status("shipped")).
  • Returning internal lists. order.items returned directly lets anyone append to it and skip your stock check. Return a tuple or a copy.
  • Ask-then-act. if acct.get_balance() >= x: acct.set_balance(acct.get_balance() - x) puts the account's logic in the caller. Tell, don't ask: acct.withdraw(x).
Predict: does the caller's change leak in?
class Playlist:
    def __init__(self):
        self._songs = []

    def add(self, song):
        if song not in self._songs:
            self._songs.append(song)

    def songs(self):
        return tuple(self._songs)      # a read-only copy


p = Playlist()
p.add("Intro")
view = p.songs()
try:
    view.append("Sneaky")
except AttributeError:
    print("can't append to a tuple")
p.add("Intro")
print(len(p.songs()))
What does it print? Show the answer
can't append to a tuple
1

Run and edit it in the app.

When to use it — and when not to

Encapsulate anything with invariants (rules that must always hold) or a representation you might change. Be relaxed about plain data carriers — a Point(x, y) record with no rules gains nothing from getters; Python's @dataclass(frozen=True) is encapsulation enough.

Pros and cons

ProsCons
Rules can't be bypassedMore methods to write and name
Internals can change freelyOverdone, it becomes boilerplate getters and setters
Easier debugging: one place changes the statePython only enforces it by convention

How it relates

Which call best follows "tell, don't ask"?

Key terms

  • Encapsulation — Keeping an object's data private and changing it only through methods that enforce its rules.
  • Invariant — A condition that must always be true for an object, such as a balance never going negative.
  • Property — A method that reads like a field (t.target), letting you add logic without changing callers.
  • Tell, don't ask — Ask an object to do the work instead of pulling its data out and deciding for it.