Visual walkthrough — Encapsulation — hiding internal state, name mangling (`__name`)
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
Childon top of an old oneBase, soChildkeeps all ofBase'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."

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.

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).

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.

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.

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.

| 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.

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."

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).