Encapsulation

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Encapsulation

1. What Is It?

is the practice of bundling an object's data (fields/attributes) and the methods that operate on that data into a single unit — the class — while controlling which parts of that unit are accessible to the outside world. It is one of the four pillars of OOP alongside , , and .

Without , any external code can freely read and modify an object's internal . This creates invisible dependencies: a BankAccount's balance field can be set to a negative number by any caller, not just the withdraw method that enforces business rules. Encapsulation enforces that changes flow through a controlled , making invariants enforceable and the class independently evolvable.

Encapsulation vs. Data Hiding: These terms are related but not synonymous. Encapsulation is the structural mechanism — bundling data and behavior together. Data hiding is a goal it enables — restricting unauthorized access to internal state. A class can be encapsulated (organized as a unit) without being tightly hidden (e.g., public fields), but strong data hiding is typically implemented through encapsulation.


QUICK CHECK

A BankAccount class exposes its balance field as public, allowing any part of the codebase to directly set it to any value, including negative numbers. A developer argues that since all the relevant data and methods are still defined inside the BankAccount class, encapsulation is intact. What is the most accurate assessment of this situation?

Choose one answer

2. How It Works

The core mechanics are:

  1. Declare fields with a restrictive access modifier (private in Java; _ or __ prefix convention in Python).
  2. Expose controlled read/write access through methods (getters/setters in Java; @property in Python).
  3. Embed validation, computation, or side effects inside those accessor methods.

Mermaid Class Diagram

Python

from dataclasses import dataclass


class BankAccount:
    def __init__(self, owner: str, initial_balance: float) -> None:
        self._owner = owner          # Convention: "internal use" — single underscore
        self._balance = initial_balance

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

    def withdraw(self, amount: float) -> bool:
        if amount <= 0 or amount > self._balance:
            return False
        self._balance -= amount
        return True

    @property
    def balance(self) -> float:
        """Read-only computed access — no setter exposed."""
        return self._balance

    @property
    def owner(self) -> str:
        return self._owner


# Usage
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.balance)   # 150.0 — accessed via @property, not direct field
# account._balance = -999  # Possible, but violates the convention

Java

public class BankAccount {
    private final String owner;   // Immutable after construction
    private double balance;

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive.");
        }
        this.balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > this.balance) {
            return false;
        }
        this.balance -= amount;
        return true;
    }

    // Getter — no setter, so balance is read-only from outside
    public double getBalance() {
        return balance;
    }

    public String getOwner() {
        return owner;
    }
}

// Usage
BankAccount account = new BankAccount("Alice", 100.0);
account.deposit(50.0);
System.out.println(account.getBalance());  // 150.0
// account.balance = -999;  // Compile error — private field

QUICK CHECK

A backend developer exposes a BankAccount class where _balance is a private field. External code needs to read the current balance but must never set it directly. Which design correctly enforces this in Python?

Choose one answer

3. Variants & Comparisons

Python Access Control

Python does not enforce access at the language level. It relies on convention.

ConventionSyntaxEnforcementPurpose
PublicattrNone — fully accessibleNormal attribute
Internal convention_attrNone — social contract onlySignal: "don't touch unless you know what you're doing"
Name mangling__attrInterpreter renames to _ClassName__attrPrevent accidental override in subclasses
Dunder (special)__attr__No manglingPython reserved protocol methods (__init__, __str__)

Name mangling with __attr does not create true privacy. obj._BankAccount__balance is still valid Python. Its primary use case is preventing subclass attribute collisions, not enforcing access control. For intent, a single underscore is the Pythonic convention.

Java Access Modifiers (most to least restrictive)

ModifierKeywordAccessible From
PrivateprivateDeclaring class only
Package-private(none)Same package
ProtectedprotectedSame package + any subclass
PublicpublicEverywhere

Python @property vs. Java getter/setter

ApproachSyntax for CallerConventionBest For
Python @propertyobj.balance (attribute-style)Decorators on methodsPythonic APIs — caller doesn't know it's a method
Java getBalance()obj.getBalance() (explicit call)JavaBeans conventionFramework compatibility (Spring, JPA, Jackson)

QUICK CHECK

A junior developer on your team defines a Python class PaymentProcessor with an attribute __transaction_id. They believe this double-underscore prefix makes the attribute truly private and inaccessible from outside the class. Which of the following best describes what actually happens?

Choose one answer

4. When to Use It (and When NOT To)

Use when:

  • A field has invariants that must be maintained (e.g., balance must be non-negative).
  • Internal representation may change without breaking callers (e.g., storing balance in cents vs. dollars internally).
  • You want to add validation, logging, or notifications on change.
  • A class's internal data structure is an implementation detail (e.g., ArrayList's backing array).

Do NOT over-encapsulate when:

  • The class is a pure data container (e.g., a value object or DTO) where all fields are inherently public.
  • Adding getters/setters for every field with no logic is just boilerplate — it provides zero protection and makes the code harder to read.
  • Python dataclasses or Java records are the right tool: if the intent is "a named bundle of data," use those instead of hand-rolling accessors.

Anti-patterns:

  • Anemic getters/setters: A class with private int x and public int getX() / public void setX(int x) with no validation is not encapsulated — it just has more verbose syntax for public fields. The field is effectively public.
  • Exposing mutable internals: Returning a reference to a mutable internal collection from a getter breaks . Return a defensive copy or an unmodifiable view instead.

Decision triggers:

  • If you see "this field must always be positive" → make it private, validate in setter.
  • If you see "caller shouldn't depend on how this is stored" → hide the field, expose a calculated property.
  • If you see a class where every setter sets a field with no logic → reconsider whether this should be a record/dataclass with public fields.

QUICK CHECK

A backend developer creates a UserProfile class with a private String email field, along with getEmail() and setEmail(String email) methods — but setEmail simply assigns the value with no validation or logic. Which of the following best describes this design?

Choose one answer

5. Real-World Usage

1. Java's java.util.ArrayList

The backing array (transient Object[] elementData) is package-private (not public) in modern Java. Callers interact only through add(), get(), remove(), etc. This lets the JDK team resize the array, change its type, or swap the implementation entirely without breaking any code that uses ArrayList. If elementData were public, any refactor would be a breaking API change.

2. Python's @property in Django ORM models

Django model fields use property-like descriptors to intercept attribute access. When you write user.email, Django's descriptor system may lazily load, validate, or translate the value. From the caller's perspective it looks like a plain attribute — that's enabling the ORM magic without changing the public API.

3. Java's LocalDate (immutable )

java.time.LocalDate stores day, month, and year as private fields and provides no setters. All "modification" returns a new instance. This makes the class thread-safe by design and ensures the date always represents a valid calendar date — the constructor validates it. Encapsulation of the fields is what makes immutability enforceable.


QUICK CHECK

The java.time.LocalDate class stores its day, month, and year as private fields and provides no setters — all 'modification' methods return a new instance instead. What is the primary benefit of this design?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. " bundles and behavior into a class and controls access to that through a defined — it's not just about making fields private, it's about ensuring state changes go through methods that can enforce invariants."
  2. "Data hiding is a goal; is the mechanism. You can have encapsulation without data hiding, but in practice they go together."
  3. "Anemic getters and setters with no validation aren't encapsulation — they're public fields with extra steps. Real encapsulation means the accessor does meaningful work."
  4. "In Python, privacy is enforced by convention, not the language — a single underscore signals intent. In Java, private is compiler-enforced."
  5. "Encapsulation directly enables the Open/Closed Principle: by hiding the internal representation, I can change it without breaking callers."

Common follow-up questions:

QuestionConcise Answer
"What's the difference between encapsulation and abstraction?"Abstraction hides what the implementation does (complexity); encapsulation hides how the data is stored (access). Both use interfaces/methods as the boundary.
"Why not just use public fields?"You lose the ability to add validation, change the internal representation, or trigger side effects without breaking all callers.
"Is Python's __attr actually private?"No. It triggers name mangling (_ClassName__attr) which prevents accidental subclass collisions — not true access restriction.
"When would you expose a mutable field?"Almost never from a class that owns it. Return an unmodifiable view or defensive copy to prevent external mutation of internal state.
"How does encapsulation relate to thread safety?"Encapsulation is a prerequisite. You can only synchronize on internal state changes if that state is private — otherwise callers can mutate fields directly, bypassing your locks.

Connections to other concepts:

  • SRP: A well-encapsulated class tends to have a single responsibility — it owns one piece of state and all methods that operate on it.
  • Open/Closed Principle: Encapsulation enables OCP — hiding internals means you can change implementation without modifying the public that other classes depend on.
  • : Encapsulation behind an interface lets you swap implementations; DIP says you should depend on that interface, not the concrete class.
  • Immutability: The strictest form of encapsulation — private fields with no setters, all state set at construction.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.