Worked examples — Encapsulation — hiding internal state, name mangling (`__name`)
Before anything, one word we lean on constantly: an attribute is just a named box glued to an object. If you write obj.balance, then balance is an attribute — a label pointing at a value stored inside obj. When Python "mangles" a name, it secretly rewrites that label to a longer one. Keep that picture: a label being quietly rewritten.

The scenario matrix
Every question this topic can ask falls into one of these cells. We will hit each row with at least one worked example.
| # | Case class | The tricky bit | Example |
|---|---|---|---|
| A | __name — plain private |
mangles to _Class__name |
Ex 1 |
| B | __name_ — one trailing _ |
STILL mangles (≤1 back) | Ex 2 |
| C | __name__ — dunder |
does NOT mangle (≥2 back) | Ex 3 |
| D | _name — single underscore |
convention only, no mangle | Ex 4 |
| E | Inheritance clash | two __data become two boxes |
Ex 5 |
| F | Access from OUTSIDE the class | mangled name typed literally works | Ex 6 |
| G | Degenerate: __ accessed in wrong class |
subclass can't see parent's __x |
Ex 7 |
| H | Real-world word problem | invariant guarded by property | Ex 8 |
| I | Exam twist: getattr/__dict__ |
mangling happens at compile time, not lookup | Ex 9 |
Row by row, the decision the interpreter makes is a tiny yes/no machine. Look at the flowchart:
Every example below is just a walk down this tree.
Ex 1 — Case A: plain __name
Forecast: Guess all three before reading. (Hint: one of them raises.)
- Identify the name.
__gold— two leading underscores, zero trailing. Why this step? We must classify the name before predicting anything; the flowchart's first two branches decide everything. - Apply the rule. Inside class
Vault,__goldbecomes ==_Vault__gold==. So the box is actually labelled_Vault__gold. Why this step? The interpreter rewrote the label at compile time; the original word__goldno longer exists anywhere as a key. - Evaluate
v.peek(). Insidepeek,self.__goldis also rewritten toself._Vault__gold— same label — so it finds the box and returns50. Why this step? Code written inside the class gets mangled the same way, so reads and writes agree. - Evaluate
v.__dict__. The stored key is'_Vault__gold', value50. Why this step?__dict__shows the real labels, which are the mangled ones. - Evaluate
v.__gold. Typed outside the class,__goldis NOT mangled (mangling only happens inside a class body), so Python looks for a box literally named__gold— none exists →AttributeError. Why this step? This is the whole "looks private" illusion.
Verify: the mangled key _Vault__gold holds 50, so getattr(v, "_Vault__gold") == 50. Checked in VERIFY.
Ex 2 — Case B: one trailing underscore __name_
Forecast: Two front, one back — does the "one back" spoil it?
- Count trailing underscores.
__x_has exactly one trailing underscore. Why this step? The rule excludes only names with two or more trailing underscores. - Apply the rule. One trailing
_is allowed, so it mangles to ==_Box__x_== (note the trailing_is kept). Why this step? Mangling rewrites the leading underscores' meaning but copies the rest of the name verbatim, trailing_included.
Verify: getattr(b, "_Box__x_") == 7. Checked in VERIFY.
Ex 3 — Case C: dunder __name__ (the exam trap)
Forecast: It has two underscores in front — surely it mangles? (It does not.)
- Count trailing underscores.
__ping__ends with two underscores. Why this step? Two-or-more trailing → the flowchart sends us to "leave unchanged." - Conclusion: no mangling. The box keeps the literal label
__ping__. Why this step? Dunders are reserved for Python's own machinery (__init__,__str__…); mangling them would break the language, so they are exempt. - Access from outside.
r.__ping__finds the un-mangled box directly and returns"pong". Why this step? Because the label was never rewritten, the outside spelling matches the stored spelling.
Verify: the literal key __ping__ exists and equals "pong". Checked in VERIFY.
Ex 4 — Case D: single underscore _name
Forecast: Only one underscore — convention, not machinery.
- Count leading underscores. Just one. Why this step? The rule needs at least two leading underscores; one is not enough.
- Conclusion. No mangling. Key stays
_tea, andc._teareturns3from anywhere. Why this step? A single_is a politeness sign ("internal, please don't touch"), enforced by nobody.
Verify: c._tea == 3. Checked in VERIFY.
Ex 5 — Case E: inheritance clash (the reason mangling exists)
Forecast: Naively, Child.__data overwrites Base.__data → both would say "child". Mangling saves us. Guess the real answers first.

- Mangle inside
Base.__datawritten inBasebecomes_Base__data = "base". Why this step? Mangling uses the class where the code is written, not the runtime class. - Mangle inside
Child.__datawritten inChildbecomes_Child__data = "child". Why this step? Different class body → different prefix → different box. revealreads_Base__data.revealis defined inBase, so itsself.__datacompiled toself._Base__data→"base". Why this step? Methods carry the mangling of their own defining class.reveal_childreads_Child__data. Defined inChild, soself.__data→self._Child__data→"child". Why this step? Same principle, other class.- Count keys. Two distinct boxes:
_Base__dataand_Child__data. Solen(k.__dict__) == 2. Why this step? Two mangled names never collide — exactly the clash-avoidance goal.
Verify: all three checked in VERIFY via a simulated class.
Ex 6 — Case F: reaching in from OUTSIDE (mangling is not security)
Forecast: If mangling were security, this would fail. Does it?
- Type the mangled name literally.
v._Vault__gold— written outside any class body, so no mangling is applied to what you typed. Why this step? Mangling only fires on names written inside a class; here you supply the final name yourself. - Look it up. The box
_Vault__goldgenuinely exists (Ex 1), so this returns50. Why this step? Nothing stops direct access — proving mangling is collision avoidance, not a lock.
Verify: getattr(v, "_Vault__gold") == 50. Checked in VERIFY.
Ex 7 — Case G: degenerate — a subclass tries to read the parent's __x
Forecast: Kid inherits from Parent, so surely it can read __secret? Watch the mangling.
- Mangle
__secretinParent. Box stored as_Parent__secret = 99. Why this step?__init__lives inParent. - Mangle
__secretinKid. Insidesteal,self.__secretis rewritten with Kid's prefix →self._Kid__secret. Why this step? The read is compiled inKid, so it uses_Kid__. - Look up
_Kid__secret. No such box exists (only_Parent__secretdoes) →AttributeError. Why this step? The two names differ by class prefix, so the subclass literally cannot name the parent's private box the "easy" way — a feature, keeping internals from leaking across the hierarchy.
Verify: simulated — looking up _Kid__secret on an object holding only _Parent__secret fails. Checked in VERIFY.
Ex 8 — Case H: real-world word problem (invariant guarded)
Forecast: Guess the Fahrenheit of before computing.
- Store internal state.
_celsius(single underscore) marks it internal. Why this step? We want controlled access, not clash-avoidance, so a property is the Pythonic tool — the underscore just discourages raw poking. - Compute Fahrenheit. . Why this step?
fahrenheitis derived, so it recomputes on every read — no stale copy. - Try the bad set.
t.celsius = -300runs the setter; →ValueError. Why this step? The setter is the guard enforcing the invariant . - Try a good set.
t.celsius = 25passes the check and stores. Why this step? Valid values flow through untouched, so callers use cleant.celsius = ...syntax.
See Properties and Descriptors and Class Invariants & Validation for the machinery behind @property.
Verify: . Checked in VERIFY.
Ex 9 — Case I: exam twist — getattr with the original name
Forecast: getattr takes a string. Does the string get mangled?
- Key fact. Mangling happens at compile time on identifiers written as code inside a class body. A string passed to
getattris never mangled. Why this step? This is the trap — mangling is a source-code rewrite, not a runtime lookup transform. getattr(v, "__gold", ...). Looks for a box literally named__gold; none exists → returns the default"MISSING". Why this step? The real box is_Vault__gold, spelled differently.getattr(v, "_Vault__gold", ...). Finds the real box → returns50. Why this step? You supplied the true (mangled) key as a string, matching the stored label.
Verify: both branches checked in VERIFY.
Recall Self-test — one per matrix row
__gold inside class Vault becomes what key? ::: _Vault__gold
Does __x_ (one trailing underscore) mangle? ::: Yes — only ≥2 trailing underscores escape.
Does __ping__ mangle? ::: No — it is a dunder (two trailing underscores).
Does _tea mangle? ::: No — needs at least two leading underscores.
In Ex 5, how many keys are in k.__dict__? ::: 2 (_Base__data and _Child__data).
Can outsiders read v._Vault__gold? ::: Yes — mangling is not security.
Why does Kid.steal() (Ex 7) fail? ::: self.__secret in Kid mangles to _Kid__secret, which doesn't exist.
Fahrenheit of C? :::
Does a string passed to getattr get mangled? ::: No — mangling is a compile-time source rewrite only.
See also: Abstraction — hiding complexity behind interfaces, Inheritance — base and derived classes, Access Modifiers (Java vs Python), and the Hinglish walkthrough 2.1.06 Encapsulation — hiding internal state, name mangling (`__name`) (Hinglish).