You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
If inheritance and polymorphism are about reusing and extending behaviour, encapsulation is about protecting it. Encapsulation is the pillar of object-oriented programming that bundles an object's data together with the methods that act on it, and then draws a boundary around that data so the outside world can only reach it through a controlled, public interface. The mechanism that draws and enforces the boundary is the system of access modifiers — private, public and protected — together with the getters and setters that police what crosses it. This lesson develops those code mechanics with traced examples, and explains why they matter, which is where the marks usually lie.
This lesson assumes the class/object basics from OOP Fundamentals. The design-level question of how to identify the right public interface for a class belongs to Object-Oriented Design in the Software Systems course; here the focus is on implementing and justifying access control in code.
Encapsulation is the pillar examiners probe most often through justification rather than recall, so it pays to study it with the question "why?" always in mind. It is easy to state that an attribute is private and to write a getter; the marks come from explaining what that buys you — a guaranteed-valid object, a stable interface, looser coupling, an easier debug. Throughout the lesson, each mechanism is paired with the reason it exists, because in the exam the reason is the part that scores, and a well-chosen reason will almost always beat a longer list of unexplained facts.
Within H446 2.2, this lesson covers encapsulation and access control. You should be able to:
-, +, #);The above paraphrases the specification; nothing is quoted verbatim.
Encapsulation is the principle of binding data (attributes) and the methods that operate on it into a single unit — the class — while restricting direct access to the object's internal state. External code does not read or write the attributes directly; it goes through the object's public interface of methods. The object becomes the sole guardian of its own data.
Two related ideas sit underneath the term, and it helps to keep them distinct:
balance and deposit belong in the same place.)You can bundle without truly hiding (a class with all-public attributes is bundled but not protected), so encapsulation in its full sense means both. The exam-critical consequence is that an object can guarantee its own validity: because every change must pass through a method, the object can reject anything that would break its rules.
A non-software analogy helps fix the idea. A vending machine bundles its stock and its cash with the operations that manage them, and it hides both behind a small interface of buttons and a coin slot. You cannot reach in and take a drink or alter the cash total directly; you must use the provided operations, which enforce the rules (correct money in, one item out, change calculated). The machine guards its own internal state. A class with private data and public methods is exactly this: the data is the stock and cash, the methods are the buttons, and the locked casing is the access modifier that turns "please do not touch the internals" into "you cannot touch the internals".
| Benefit | Explanation |
|---|---|
| Data protection | Private attributes cannot be set to invalid values by external code. |
| Controlled access | Getters/setters can validate, transform or log on every read or write. |
| Reduced coupling | Callers depend on the stable public interface, not on internal details. |
| Easier maintenance | The internal implementation can change without breaking any caller. |
| Easier debugging | A wrong value must have come through a method, so the search is localised. |
Exam Tip: Encapsulation is not merely "make attributes private". The examinable substance is controlled access and information hiding — validation, a stable interface, and the freedom to change internals. Always explain these benefits, not just the definition.
Access modifiers (visibility modifiers) declare which parts of a program may reach an attribute or method. Three levels appear in the specification, each with a standard UML symbol.
| Modifier | UML symbol | Accessible from | Python convention |
|---|---|---|---|
| Public | + | Anywhere — inside and outside the class | self.name |
| Private | - | Only inside the declaring class | self.__name |
| Protected | # | Inside the class and its subclasses | self._name |
The general rule of good design is to make attributes as private as possible and expose only what callers genuinely need — the principle of least privilege applied to class members. Protected sits between the two: hidden from the outside world but still reachable by subclasses, which is useful when a subclass legitimately needs the parent's state (as the Vehicle hierarchy's _make/_model did in the previous lesson).
Python is unusual in that it does not enforce access at the language level; it relies on naming conventions plus one mechanical trick.
class Employee:
def __init__(self, name: str, salary: float, department: str):
self.name = name # public
self._department = department # protected (by convention)
self.__salary = salary # private (name mangling)
e = Employee("Sam", 35000, "Sales")
print(e.name) # Sam -- public: fine
print(e._department) # Sales -- works, but you are signalled not to
# print(e.__salary) # AttributeError -- name mangled
print(e._Employee__salary) # 35000 -- the mangled name still reaches it
self.name): reachable anywhere.self._department): a single leading underscore is a signal ("internal — keep out") that Python does not actually prevent; disciplined programmers respect it.self.__salary): a double leading underscore triggers name mangling — Python rewrites the attribute to _Employee__salary, so casual e.__salary fails. As the last line shows, it is not true security, merely a strong "do not touch" that prevents accidental access and name clashes in subclasses.| Modifier | Same class | Same package | Subclass | Everywhere |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
| (default/package) | Yes | Yes | No | No |
private | Yes | No | No | No |
In Java the compiler enforces these — private data simply cannot be touched from outside, full stop. The contrast is a good exam point: encapsulation is a principle; some languages enforce it in the compiler, others (Python) rely on convention, but the design intent is identical.
A common exam task is to justify a choice of modifier for a given member. Work from the principle of least privilege — start everything private and open it up only as far as a real requirement demands:
| Situation | Sensible choice | Reason |
|---|---|---|
| Internal state (balance, age, ID) | Private | No outside code has any business writing it directly. |
An operation callers must invoke (deposit, withdraw) | Public | It is the interface; it must be callable. |
State a subclass legitimately needs (_make in Vehicle) | Protected | Hidden from the outside, but reachable down the hierarchy. |
A helper used only inside the class (__validate, __hash) | Private | An implementation detail; exposing it would widen the interface needlessly. |
The reasoning the markers want is not "I made it private because privacy is good", but a justification tied to who needs access: the balance is private because only the account's own methods should change it; deposit is public because external code must be able to pay in; a validation helper is private because it is an internal detail no caller should depend on. Phrasing the answer as "X is private/public/protected because these parties need access and those do not" is what scores.
Getters (accessors) return the value of a private attribute; setters (mutators) change it, normally after validation. Together they form the controlled public interface to an object's data — the gateway through which the outside world is allowed to interact with otherwise-hidden state.
# Weak: setters that merely assign add almost nothing over public attributes
class Person:
def __init__(self, name: str, age: int):
self.__name = name
self.__age = age
def get_age(self) -> int:
return self.__age
def set_age(self, age: int):
self.__age = age # no guard -> any value accepted
# Strong: validation makes the setter earn its place
class Person:
def __init__(self, name: str, age: int):
self.__name = name
self.set_age(age) # route the initial value through validation too
def get_name(self) -> str:
return self.__name
def get_age(self) -> int:
return self.__age
def set_name(self, name: str):
if not name.strip():
raise ValueError("Name cannot be empty")
self.__name = name.strip()
def set_age(self, age: int):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError("Age must be between 0 and 150")
self.__age = age
Trace an attempted misuse: p = Person("Alice", 17) sends 17 through set_age, which checks it is an int and in range, then stores it. A later p.set_age(200) raises ValueError before the bad value can land, so p's age is never out of range. Crucially the constructor calls set_age rather than assigning self.__age directly, so the rule lives in exactly one place and applies even to the very first value — there is no back door.
Exam Tip: A setter that just assigns is barely better than a public attribute. Markers look specifically for validation inside setters as the evidence that you understand why encapsulation is worth the effort.
The pay-off that most clearly separates a strong answer is information hiding — because callers depend only on the public interface, the internal representation can change completely without breaking any of them.
# Version 1: stores the temperature in Celsius
class Thermometer:
def __init__(self):
self.__temp_celsius = 0
def get_celsius(self) -> float:
return self.__temp_celsius
def get_fahrenheit(self) -> float:
return self.__temp_celsius * 9/5 + 32
def set_celsius(self, temp: float):
if temp < -273.15:
raise ValueError("Below absolute zero")
self.__temp_celsius = temp
# Version 2: now stores Fahrenheit internally -- SAME public interface
class Thermometer:
def __init__(self):
self.__temp_fahrenheit = 32
def get_celsius(self) -> float:
return (self.__temp_fahrenheit - 32) * 5/9
def get_fahrenheit(self) -> float:
return self.__temp_fahrenheit
def set_celsius(self, temp: float):
if temp < -273.15:
raise ValueError("Below absolute zero")
self.__temp_fahrenheit = temp * 9/5 + 32
The stored unit flipped from Celsius to Fahrenheit, yet get_celsius, get_fahrenheit and set_celsius keep the same signatures and behaviour. Any code written against Version 1 runs unchanged against Version 2 — it cannot even tell the difference, because it never had access to the hidden attribute. Had temp_celsius been public, every caller that read it directly would now be broken. This is the concrete, examinable meaning of "reduced coupling" and "easier maintenance": the hidden boundary is exactly what lets the internals evolve.
Two quality measures explain why the Thermometer change was painless, and bringing them into an answer signals real depth. Coupling measures how strongly one part of a program depends on another; cohesion measures how strongly the contents of a single unit belong together. Good design aims for low coupling and high cohesion, and encapsulation pushes both in the right direction.
get_celsius and friends — so changing the stored unit could not reach them. Direct attribute access, by contrast, tightly couples callers to a representation, so any change ripples outward. Encapsulation is the primary tool for keeping coupling low.A useful one-liner for the exam: encapsulation lowers coupling (callers depend on the interface, not the internals) and raises cohesion (related data and behaviour live together), which is exactly why encapsulated code is easier to change and maintain. Naming coupling and cohesion explicitly, and linking each to the encapsulation mechanism, lifts an answer above the generic "it's easier to maintain".
class BankAccount:
def __init__(self, balance):
self.balance = balance # public: anyone can write anything
account = BankAccount(1000)
account.balance = -5000 # allowed! no validation
account.balance = "hello" # allowed! wrong type entirely
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.