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:
- 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. - 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# enforceprivate). - 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.
In code
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.0Run 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(), notset_status("shipped")). - Returning internal lists.
order.itemsreturned directly lets anyoneappendto 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).
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
1Run 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
| Pros | Cons |
|---|---|
| Rules can't be bypassed | More methods to write and name |
| Internals can change freely | Overdone, it becomes boilerplate getters and setters |
| Easier debugging: one place changes the state | Python only enforces it by convention |
How it relates
- Encapsulation is what makes Classes and Objects worth having.
- Low coupling mostly is good encapsulation: callers depend on behaviour, not data layout.
- The smell Inappropriate Intimacy and the refactoring Encapsulate Field (Organizing Data) are the cure when it's missing.
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.