skip to content

A C library's public header contains only `typedef struct Conn Conn;` plus functions taking `Conn *` — the struct itself is defined in a .c file the caller never sees — even though the C language has no access-control keywords at all. Python is similar in spirit: a leading underscore is a convention, `__all__` names the exported surface, and an attribute written `self.__x` inside `class Cls` is simply stored under the name `_Cls__x` so that a subclass will not collide with it. Meanwhile, a class that declares every field private and adds a matching getter and setter for each satisfies every rule a language is able to check. Are encapsulation and information hiding the same thing? Argue using these cases.

level: middleimportance: must knowfreq 55%

answer

  1. mechanism vs secret — two axes
  2. C opaque struct: no keyword, total hiding
  3. getter+setter = republished representation
  4. `__x` mangling = collision avoidance
  5. Go/Rust hide per module, not per type

basics

~20 s

No. Encapsulation is a mechanism: bundle state with the operations that reach it. Information hiding is a design property: a module keeps a changeable decision secret. A C header can hide everything with no keywords; getter-and-setter classes hide nothing.

solid answer

~60 s

They are two different axes. - **Hiding with no mechanism.** The C header declares `typedef struct Conn Conn;` and defines the struct only in the .c file. C offers no access keyword, yet a client cannot name a field, cannot embed the struct by value, cannot even take its `sizeof`. The representation is a genuine secret: change it, rebuild that one file, and no caller changes. - **Mechanism with no secret.** A class with a private field plus a getter and setter for each field routes every access through a method — but it has republished the representation as the interface. Change a field's type or drop it and every client changes. Encapsulated, not hiding. - **Hiding by published surface.** Python's leading underscore and `__all__` state what the contract is; `__x` mangling exists to avoid subclass collisions, not to block anyone. - **A different unit entirely.** Go has no `private`: capitalisation exports, and the *package*, not the type, is the boundary. Parnas's test: which changeable decision does this module keep secret?

code

text · 11 lines
text
// public surface (what callers see)
type Conn        // name only; no fields, no size
open(host) -> Conn*
send(Conn*, bytes)
close(Conn*)

// private implementation (never shipped to callers)
struct Conn { socket; buffer; retries; deadline }

// caller cannot: read a field, embed Conn by value, take sizeof(Conn)
// implementer may: add/remove/reorder fields, rebuild one file, ship

go deeper

for a junior

Recall the two definitions and one clean counterexample: a class with a getter and setter for every field satisfies the language rules but hides nothing, because changing a field changes the interface.

for a middle

Own both directions. Show hiding with no mechanism (a C header that names a type without defining it) and mechanism with no hiding (accessor pairs), and state the operational test: can I change the decision without touching clients?

for a senior

Frame it as Parnas's question — which changeable decision is this module's secret — and connect it to real churn: which of your modules survives a representation change, and which forces a fan-out of edits. Note that the hiding unit differs by language.

for a principal

Treat it as a decomposition and blast-radius question. Decide where secrets sit relative to package/module boundaries, accept the costs hiding imposes (allocation moves inside the library, no inlining across the boundary, coarser APIs), and set conventions for the published surface where the language enforces nothing.

## Two ideas that share one slogan **Encapsulation** is a language construct. State and the operations that mediate it are bundled into a single unit, and access to that state is routed through those operations. It answers *how*. **Information hiding** is David Parnas's 1972 decomposition criterion: split a system so that each module conceals a design decision that is likely to change, and design the interface to reveal as little of that decision as possible. It answers *what is secret*. In Parnas's own worked example, the interesting modularization was the one where no client could tell whether lines of text were stored expanded or computed on demand — the storage decision was the secret. They travel together in one popular language family, which is why they get taught as synonyms. Look at several languages at once and they come apart in both directions. ## The operational test Hiding has exactly one test: **can I change this decision without changing any client?** Not "is there a keyword", not "is there a method in front of it" — can the decision move. If changing a field's type, its units, its representation, or its very existence forces edits in calling code, that decision was never a secret, whatever the declaration said. ## Four quadrants - **Mechanism and secret.** An opaque handle behind a small function set; an OCaml module exposing an abstract `type t`. - **Mechanism, no secret.** The private-field-plus-getter-and-setter class. Every access is mediated, so the language is satisfied, but the field set *is* the interface. - **Secret, no mechanism.** The C opaque struct; a Python package whose internals churn freely between releases. - **Neither.** A struct in a shared header whose fields everyone reads and writes directly. ## C: hiding without any access mechanism The opaque-pointer idiom is the purest case. The header declares an *incomplete type*: the name exists, the layout does not. Because the layout is unknown to the compiler while compiling client code, the client cannot declare a variable of that type by value, cannot embed it in another struct, cannot compute its size, and cannot dereference a field. Everything must go through the declared functions. C spent zero syntax on access control and got total representational hiding — and it got it as a consequence of the compilation model rather than a permission list. The cost is the mirror image: allocation must move inside the library (`conn_open`/`conn_close`), and nothing can be stack-allocated or inlined by the client. Hiding is a trade, not a free win. ## Python: the published surface is the contract Python's conventions are a documentation system. A single leading underscore is a statement of intent. `__all__` narrows `from mod import *` and, more usefully, tells a reader which names the module considers its contract. Double leading underscores trigger *name mangling*: inside `class Cls`, `self.__x` is compiled to `self._Cls__x`. That mechanism was introduced so that a base class and a subclass writing the same attribute name would not clobber each other — collision avoidance in an inheritance chain, not a wall. And it works, because hiding is enforced by the dependency graph in practice rather than by the compiler. Mature Python libraries restructure internals across releases and users are fine — until someone reaches past the published surface into a private helper, at which point an upgrade breaks them. That is precisely the Parnas failure mode arriving with no access modifier involved. ## Go and Rust: the module, not the type, is the unit Go removed the keyword. An identifier beginning with a capital letter is exported from its package; anything else is not. Inside the package every file sees every lowercase field of every type, so Go has essentially *no per-type hiding* and very good package-level hiding — coherent, because Parnas's module was never a class. Go goes further with `internal/` directories, which restrict who may even import a package. Rust is module-scoped too: items and struct fields are private to their module by default, `pub` widens them outward, `pub(crate)` bounds them to the crate, and a child module can reach its ancestors' private items. A single Rust module can therefore contain several cooperating types that share representation freely while the crate sees only a narrow surface — a shape that a strictly per-class privacy model cannot express without leaking. ## ML-family signatures: the secret is a typing fact In OCaml or Standard ML, a `.mli` signature can declare `type t` with no right-hand side. Outside the module, `t` is abstract: values of it can be passed around but nothing about their representation is known. Violations are not access errors, they are *type* errors — the representation is unknowable rather than forbidden. Same goal, entirely different machinery. ## Why the accessor-pair class fails A getter and setter per field is the canonical anti-example. It satisfies every syntactic rule, but the client now depends on the exact field set, their types, and their units. Swapping two `int` coordinates for a polar representation, or a `Date` for an instant, changes the interface. Encapsulation was achieved; nothing was hidden. ## How to answer in an interview Define both in one sentence each. Then give the two-way counterexample: something with no mechanism that hides completely (the C opaque struct, or a Python package with a disciplined surface), and something with full mechanism that hides nothing (the accessor-pair class). Close on the operational test, and note that the unit of hiding differs by language — package in Go, module in Rust and ML, class in the Java/C# family — because that is where most real hiding decisions actually get made.

  • Give an example of a module with a fully access-controlled interface that still leaks a design decision.
    A repository whose method names and return types mirror the database table columns one-for-one: every field is private and reached through methods, but the schema is the interface, so a column rename ripples through every caller. Returning an internal mutable collection directly does the same, since callers can then depend on its identity and mutability. Exposing a raw number without stating units leaks the storage choice too — callers start hard-coding conversions.
  • Go and Rust make the package/module the privacy unit rather than the type. What does that buy and cost?
    It buys cohesion: several types that genuinely share one secret can cooperate over each other's internals without exposing anything outside the module, which a strictly per-class model can only do by widening visibility or adding friend-style escape hatches. The cost is that privacy inside a large package is purely a matter of discipline, so packages must be kept small to stay meaningful. Go's `internal/` directories and Rust's `pub(crate)` exist to add a second, coarser ring around that.
  • How would you check whether a module's secret is actually secret?
    Change the decision and see who moves. Grep for callers that name the internal concept, count how many files a representation change forces you to edit, and check whether the interface would still make sense under a different implementation. If the type signatures of the public surface mention the representation — the column names, the units, the concrete collection type — the secret already escaped.

A vending machine enforces access — you must use the slot — yet hides nothing: the glass front shows the exact stock and layout. An unlisted phone number enforces nothing at all, yet nobody calls. Mechanism and secrecy are separate properties.

saying these in an interview costs you the question

  • Saying encapsulation and information hiding are two words for the same thing.
  • Claiming a getter/setter for every field achieves information hiding — it republishes the representation as the contract.
  • Believing Python's `__x` exists to block access; it is name mangling to avoid subclass collisions.
  • Assuming hiding requires an access keyword, so languages without one (C, Go, Python) 'have no encapsulation'.
  • Assuming the class is universally the unit of hiding, when Go, Rust and ML hide at module/package level.

context