2.1.6 · D3OOP Fundamentals

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

2,429 words11 min readBack to topic

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.

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

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:

no

yes

yes

no

identifier written in class body

starts with two underscores

leave unchanged

ends with two or more underscores

rewrite to underscore Class underscore underscore name

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

  1. 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.
  2. Apply the rule. Inside class Vault, __gold becomes ==_Vault__gold==. So the box is actually labelled _Vault__gold. Why this step? The interpreter rewrote the label at compile time; the original word __gold no longer exists anywhere as a key.
  3. Evaluate v.peek(). Inside peek, self.__gold is also rewritten to self._Vault__gold — same label — so it finds the box and returns 50. Why this step? Code written inside the class gets mangled the same way, so reads and writes agree.
  4. Evaluate v.__dict__. The stored key is '_Vault__gold', value 50. Why this step? __dict__ shows the real labels, which are the mangled ones.
  5. Evaluate v.__gold. Typed outside the class, __gold is 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?

  1. Count trailing underscores. __x_ has exactly one trailing underscore. Why this step? The rule excludes only names with two or more trailing underscores.
  2. 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.)

  1. Count trailing underscores. __ping__ ends with two underscores. Why this step? Two-or-more trailing → the flowchart sends us to "leave unchanged."
  2. 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.
  3. 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.

  1. Count leading underscores. Just one. Why this step? The rule needs at least two leading underscores; one is not enough.
  2. Conclusion. No mangling. Key stays _tea, and c._tea returns 3 from 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.

Figure — Encapsulation — hiding internal state, name mangling (`__name`)
  1. Mangle inside Base. __data written in Base becomes _Base__data = "base". Why this step? Mangling uses the class where the code is written, not the runtime class.
  2. Mangle inside Child. __data written in Child becomes _Child__data = "child". Why this step? Different class body → different prefix → different box.
  3. reveal reads _Base__data. reveal is defined in Base, so its self.__data compiled to self._Base__data"base". Why this step? Methods carry the mangling of their own defining class.
  4. reveal_child reads _Child__data. Defined in Child, so self.__dataself._Child__data"child". Why this step? Same principle, other class.
  5. Count keys. Two distinct boxes: _Base__data and _Child__data. So len(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?

  1. 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.
  2. Look it up. The box _Vault__gold genuinely exists (Ex 1), so this returns 50. 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.

  1. Mangle __secret in Parent. Box stored as _Parent__secret = 99. Why this step? __init__ lives in Parent.
  2. Mangle __secret in Kid. Inside steal, self.__secret is rewritten with Kid's prefix → self._Kid__secret. Why this step? The read is compiled in Kid, so it uses _Kid__.
  3. Look up _Kid__secret. No such box exists (only _Parent__secret does) → 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.

  1. 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.
  2. Compute Fahrenheit. . Why this step? fahrenheit is derived, so it recomputes on every read — no stale copy.
  3. Try the bad set. t.celsius = -300 runs the setter; ValueError. Why this step? The setter is the guard enforcing the invariant .
  4. Try a good set. t.celsius = 25 passes the check and stores. Why this step? Valid values flow through untouched, so callers use clean t.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?

  1. Key fact. Mangling happens at compile time on identifiers written as code inside a class body. A string passed to getattr is never mangled. Why this step? This is the trap — mangling is a source-code rewrite, not a runtime lookup transform.
  2. 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.
  3. getattr(v, "_Vault__gold", ...). Finds the real box → returns 50. 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).