2.1.6 · D2OOP Fundamentals

Visual walkthrough — Encapsulation — hiding internal state, name mangling (`__name`)

1,795 words8 min readBack to topic

Nothing here assumes you already know classes deeply. We only need three plain ideas, and I define each before I use it:

  • attribute — a piece of data glued to an object, reached by a dot: robot.battery. Think "a labelled drawer inside a box."
  • class — the blueprint that says which drawers an object has and what its buttons (methods) do.
  • inheritance — building a new blueprint Child on top of an old one Base, so Child keeps all of Base's drawers and buttons and can add more.

Everything else we earn as we go.


Step 1 — An object is a box of labelled drawers

WHAT. Before any mangling, picture what an object actually is at runtime: a little dictionary — a set of label → value pairs. Python even shows it to you as obj.__dict__.

WHY. You cannot understand a collision until you see that every attribute is stored under a plain text key. Two attributes with the same key genuinely overwrite each other, the way two files with the same name overwrite in a folder. Mangling exists only to change those keys.

PICTURE. Look at the figure. The red drawer is the one we care about: it is stored under the literal string "battery". Reaching it means "go to the box, find the drawer labelled battery."

Figure — Encapsulation — hiding internal state, name mangling (`__name`)


Step 2 — One class, one drawer: no problem yet

WHAT. A single class Base writes self.__id = 7. Right now, imagine mangling does not exist. The key is just the string "__id".

WHY. We start with the innocent case so that when it breaks in Step 3 you feel the exact moment it goes wrong. With one class there is nothing to clash with, so the naive key "__id" works fine.

PICTURE. The box has one red drawer labelled __id. A method reveal() reads that same label back. Arrow in, arrow out — they point at the same drawer, so everything is consistent.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)

Here reveal() also reads self.__id, i.e. it looks up the same string "__id". Read and write agree. No pain yet.


Step 3 — The collision (the disaster the rule prevents)

WHAT. Now Child inherits from Base. A Child object is one single box that holds both classes' drawers. Base.__init__ writes a drawer named __id for its bookkeeping; the programmer of Child, unaware, also writes self.__id for a completely different purpose.

WHY. This is the whole reason mangling exists. Because both use the same string key "__id", they are the same drawer. Whoever writes last wins, silently. Base's methods still say "read __id" — but they now read Child's value. Base breaks and nobody typed a bug.

PICTURE. One box. Base puts a value in the red drawer __id. Then Child puts a different value in that same red drawer, overwriting it. Base.reveal() points at the red drawer and gets the wrong thing (dashed red = the value that got clobbered).

Figure — Encapsulation — hiding internal state, name mangling (`__name`)


Step 4 — The fix: stamp the class name onto the key

WHAT. The interpreter's cure: whenever it compiles the code inside a class body and sees a name starting with __, it rewrites that name to include the class name. Inside Base, __id becomes _Base__id. Inside Child, __id becomes _Child__id.

WHY. Two different strings can never be the same drawer. By stamping the class it was written in onto the key, each class gets its own private drawer that no sibling or child can accidentally hit. The collision from Step 3 becomes impossible.

PICTURE. The naming machine (red) takes the raw name __id on the left and, knowing the class is Base, emits _Base__id on the right. This happens once, when the code is read, not when it runs.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)

So __id inside Base_Base__id; inside Child_Child__id. Every symbol here is just string concatenation — no magic, no hashing.


Step 5 — Two boxes-within-a-box, no collision

WHAT. Re-run Step 3, but with the rewrite from Step 4. Base stores under _Base__id; Child stores under _Child__id. Both live in the same Child.__dict__, but under different keys.

WHY. This is the payoff. Base.reveal() was compiled inside Base, so its self.__id also became self._Base__id — it reads its own drawer and is untouched by Child. Child reads _Child__id. Each method quietly refers to its own version. The Step 3 disaster is gone.

PICTURE. One box, now with two distinct drawers: red _Base__id and black _Child__id. Base.reveal()'s arrow lands on the red drawer; Child's method lands on the black one. No arrow crosses.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)

Two keys ⇒ two drawers ⇒ two survivors. This is exactly Worked Example 2 in the parent note.


Step 6 — When it does not fire: the exact boundary

WHAT. The rule is picky about underscores. It fires only for names with ≥ 2 leading underscores and ≤ 1 trailing underscore. We test all four cases.

WHY. Learners over-apply mangling and get surprised that __init__ isn't renamed. The trailing-underscore clause is the reason. You must see every case so no code ever surprises you.

PICTURE. Four names go into the machine. Green "pass through the machine (mangled)" for __x and __x_; the red name __x__ is a dunder — it bypasses the machine untouched; _x (single leading underscore) also bypasses.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)
Name you type Leading _ Trailing _ Mangled? Becomes
__x 2 0 Yes _C__x
__x_ 2 1 Yes _C__x_
__x__ 2 2 No (dunder) __x__
_x 1 0 No _x

Step 7 — The catch: it's a name tag, not a lock

WHAT. From outside the class, mangling does not run (there's no enclosing class body to stamp). So a.__balance looks for the literal key "__balance", which doesn't exist → AttributeError. But typing the mangled name a._Account__balance reaches straight in.

WHY. People mistake the AttributeError for security. It isn't. The data was only renamed, and you can spell the new name yourself. Mangling prevents accidents, never intruders.

PICTURE. From inside the class the machine stamps names, so code lands on the real drawer. From outside, no machine runs: a.__balance misses, but the literal a._Account__balance hits the same red drawer directly.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)

The one-picture summary

The whole story on one canvas: same class → same key → collision (top). Add the compile-time stamp → different keys → two survivors (bottom). The red path is the failure; the fix is simply "make the keys different by stamping the class name."

Figure — Encapsulation — hiding internal state, name mangling (`__name`)
Recall Feynman retelling — the walkthrough in plain words

An object is a box of labelled drawers (Step 1). One class writing to one drawer is fine (Step 2). But when a Child is built on a Base, they share one box; if both invent a drawer called __id, the second one written just paints over the first, and nobody sees the bug (Step 3). Python's fix is a little naming machine that runs while it reads your class: any __name gets the class's own name stamped on it, so Base's becomes _Base__id and Child's becomes _Child__id (Step 4). Now they're two separate drawers in the same box, and each method reads its own (Step 5). The machine is fussy: it only fires for names with two underscores in front and at most one in back — so __init__ (two in back) is left alone (Step 6). And it's only a name tag: from outside, anyone who spells _Account__balance walks right in, so it was never a lock (Step 7).


Connections: parent → OOP Fundamentals · read alongside Class Invariants & Validation, Properties and Descriptors, Inheritance — base and derived classes, Abstraction — hiding complexity behind interfaces · Hinglish version → 2.1.06 Encapsulation — hiding internal state, name mangling (`__name`) (Hinglish).