In Defense of Encapsulation

What a Python Fan in My Class Taught Me About Why C++ Still Matters


            

Last week, during a lecture on access specifiers, one of my brightest students stopped me mid-sentence. “Professor,” she said, “this is all obsolete. In the modern world we don’t need encapsulation. Look at Python — there is no private, no public, no ceremony, and it powers half the internet, all of machine learning, and most of the startups you keep telling us to admire.”

She is, by her own cheerful admission, a Python fan. And I want to be fair to her position before I dismantle it, because she is half right. Python is a magnificent language for rapid prototyping. When I need to test an idea at two in the morning, I reach for Python too. It gets out of your way. It lets you go from a blank file to a working pipeline before C++ has finished arguing with you about your CMakeLists.txt.

But “I can build a prototype quickly” and “encapsulation is obsolete” are two completely different claims. The first is true. The second confuses a language that chose not to enforce encapsulation with a world that no longer requires it. So let me make the case, with as many real examples as I can fit, for why encapsulation is not a relic of the 1990s but one of the most important ideas in software engineering — and why C++ enforcing it at compile time is a feature, not a bureaucratic inconvenience.

First, what encapsulation actually is (and isn’t)

My student’s mistake is one I see constantly: she thinks encapsulation means “typing the word private.” It doesn’t. Encapsulation is the discipline of bundling data together with the operations allowed on that data, and then guaranteeing that the data can only ever be in a valid state. The private keyword is just the tool C++ gives you to make that guarantee unbreakable.

The key word is guarantee. Python offers you a polite request. C++ offers you a contract the compiler will enforce. That difference sounds small in a 50-line script. It becomes the difference between a maintainable system and a haunted house when you have 500,000 lines and 30 engineers.

Example 1: The “we’re all adults here” philosophy and where it breaks

Python’s design philosophy is often summarized as “we’re all consenting adults here.” If you prefix an attribute with an underscore, you’re signaling “please don’t touch this.” Here is the entire enforcement mechanism:

class BankAccount:
    def __init__(self, balance):
        self._balance = balance   # the underscore is a polite suggestion

acct = BankAccount(100)
acct._balance = -999999          # Python: "Sure, why not."

Nothing stopped that. The underscore is a sticky note that says “please don’t.” It works right up until a tired developer on a deadline, or a clever junior who “just needs to fix it quickly,” reaches past it. And they will. I have twenty years of evidence that they will.

Now the C++ version:

class BankAccount {
    long long balance_cents_;   // private by default in a class

public:
    explicit BankAccount(long long initial_cents) {
        if (initial_cents < 0)
            throw std::invalid_argument("balance cannot start negative");
        balance_cents_ = initial_cents;
    }

    void withdraw(long long cents) {
        if (cents > balance_cents_)
            throw std::runtime_error("insufficient funds");
        balance_cents_ -= cents;
    }

    long long balance() const { return balance_cents_; }
};

BankAccount acct(100);
// acct.balance_cents_ = -999999;  // COMPILE ERROR. The program does not even build.

That last line is the whole argument in one sentence. In Python, the invalid state is reachable and the bug ships. In C++, the invalid state is unrepresentable, and the mistake is caught before the program even exists as an executable. My student called that “ceremony.” I call it the compiler doing my code review for free, at three in the morning, when no human reviewer is awake.

Example 2: Class invariants — the thing Python literally cannot promise

An invariant is a statement that is always true about an object for its entire lifetime. “A temperature in Kelvin is never negative.” “A date is always a real calendar date.” “This fraction is always stored in lowest terms.” Encapsulation is the mechanism that makes invariants provable.

class Temperature {
    double kelvin_;

    void validate() const {
        if (kelvin_ < 0.0)
            throw std::domain_error("below absolute zero is physically impossible");
    }

public:
    explicit Temperature(double kelvin) : kelvin_(kelvin) { validate(); }

    double in_celsius() const    { return kelvin_ - 273.15; }
    double in_fahrenheit() const { return kelvin_ * 9.0/5.0 - 459.67; }
};

Because kelvin_ is private and the only way to set it runs through the constructor’s validate(), I can state with mathematical confidence: there does not exist a Temperature object anywhere in this program that is below absolute zero. I don’t have to check. I don’t have to hope. The class structure makes it impossible.

In Python, with a public attribute, that guarantee evaporates the moment anyone writes t._kelvin = -50. You can layer on @property getters and setters to simulate this — and Python programmers often do, which is itself a quiet admission that encapsulation is useful — but it remains a convention enforced by goodwill, not by the language. The class can only ask to be respected. It cannot require it.

Example 3: RAII — encapsulation applied to resources, and C++’s crown jewel

This is where I would ask my student to put down her coffee, because this is the example that has converted more skeptics than any other. Encapsulation in C++ isn’t only about data validity — it’s about resource ownership. The pattern is called RAII (Resource Acquisition Is Initialization), and it is something Python simply does not have in the same form.

Consider a file handle, or a mutex lock — something that must be released, no matter what happens. In Python you reach for try/finally or a context manager, and you must remember to use it:

# Python: if you forget the 'with', or an exception escapes, the lock leaks
lock.acquire()
do_something_that_might_throw()   # if this throws, the next line never runs
lock.release()                    # leaked lock &rarr; deadlock

In C++, the encapsulated lock releases itself automatically when it goes out of scope — even if an exception is thrown, even if the function returns from ten different places:

void transfer(Account &from, Account &to, long long cents) {
    std::lock_guard < std::mutex > guard(from.mutex());  // lock acquired here

    if (from.balance() < cents)
        throw std::runtime_error("insufficient funds");  // lock STILL released

    from.withdraw(cents);
    to.deposit(cents);
}   // &larr; guard's destructor runs here, AUTOMATICALLY, on every exit path

The std::lock_guard encapsulates the lock so thoroughly that releasing it is no longer something a human can forget to do. The same idea powers std::unique_ptr for memory, std::fstream for files, and every well-designed resource type in modern C++. The resource’s lifetime is welded to a scope. This is encapsulation as a safety mechanism for the physical world of memory, sockets, and file descriptors — and it is deterministic, which Python’s garbage collector is not. My student admires Rust’s borrow checker; she should know that Rust’s ownership model is RAII wearing a fancier hat.

Example 4: The freedom to change your mind without breaking the world

Here is the argument that matters most once you leave the classroom and join a real team. Encapsulation lets you change how a class works internally without forcing everyone who uses it to rewrite their code.

Suppose I write an Angle class. Today I store it in degrees:

class Angle {
    double degrees_;
public:
    explicit Angle(double deg) : degrees_(deg) {}
    double degrees() const { return degrees_; }
    double radians() const { return degrees_ * 3.14159265358979 / 180.0; }
};

Some time later I might realize that for performance and numerical accuracy I should store radians internally. I change the private representation:

class Angle {
    double radians_;   // representation changed!
public:
    explicit Angle(double deg) : radians_(deg * 3.14159265358979 / 180.0) {}
    double degrees() const { return radians_ * 180.0 / 3.14159265358979; }
    double radians() const { return radians_; }
};

Every single piece of code in the entire codebase that used Angle continues to compile and run unchanged, because they only ever touched the public interfacedegrees() and radians() — never the private field. The interface was the promise; the field was the secret. Encapsulation is what let me keep my promise while changing my secret.

If degrees_ had been a public attribute that hundreds of call sites read directly — the normal state of affairs in a culture that considers encapsulation obsolete — that refactor becomes a multi-week archaeological dig through the codebase, hoping you found every usage. I have lived that nightmare. Encapsulation is how you avoid it.

Example 5: const-correctness — a guarantee Python can’t even express

C++ goes a step further and lets you encapsulate intent. A function can promise, in its signature, that it will not modify an object:

void print_statement(const BankAccount &acct) {
    std::cout << acct.balance();   // reading is fine
    // acct.withdraw(50);          // COMPILE ERROR: cannot modify a const object
}

When I pass an account to print_statement, the const guarantees — at compile time — that my balance cannot be touched. Python has no equivalent. You hand an object to a function and you simply trust that it behaves. In a large system, “simply trust” is not an engineering strategy; it’s a prayer.

So is C++ “better” than Python? No. That’s the wrong question.

I want to be honest with my student and with you, because I am a teacher and not a salesman. Python’s lack of enforced encapsulation is not an oversight — it is a deliberate, intelligent trade-off. By trusting the programmer instead of constraining them, Python becomes wonderfully fast to write, easy to learn, and ideal for scripts, data exploration, glue code, and exactly the rapid prototyping my student loves. For a 200-line analysis notebook, the compiler ceremony of C++ would be pure overhead, and I would happily reach for Python myself. Different jobs, different tools.

But “this trade-off is right for prototypes” is the opposite of “encapsulation is obsolete.” The features Python omits for speed of writing are precisely the features you start to crave the moment your prototype succeeds and grows into a system that flies aircraft, runs an exchange, controls a pacemaker, or renders a frame in 16 milliseconds while a million users watch. At that scale you do not want a language that politely asks people to behave. You want a language that makes incorrect states impossible to compile.

Encapsulation is not about distrusting your colleagues. It is about respecting them enough to give them guarantees instead of suggestions, so that the next engineer — possibly you, six months from now, with no memory of why this code exists — cannot accidentally reach into the machine and break it. That need has not disappeared in the modern world. If anything, as our systems grow larger and our teams grow bigger and our deadlines grow shorter, that need has never been greater.

My student is brilliant, and one day she will lead a team. When her prototype graduates into a product that real people depend on, she will rediscover encapsulation on her own — every excellent engineer eventually does. I’m just hoping to save her a few production outages along the way.

Now, about that CMakeLists.txt

Categories: Programming Tags: ,

Writing Your First ISR — Rules, Pitfalls, and the volatile Keyword

In Part 1 we established what interrupts are and why they exist. Now it is time to write actual interrupt service routines (ISRs) and learn the rules that keep them working correctly. This is where many beginners run into mysterious bugs — code that works fine until an interrupt fires, then behaves unpredictably. Almost always, […]

Comments are closed.