Exercises — Encapsulation — hiding internal state, name mangling (`__name`)
The one rule we lean on the whole way down:

Level 1 — Recognition
Goal: can you spot which names get mangled, and what they become?
L1.1 — Which names get mangled?
For a class named Widget, decide for each identifier whether it is mangled, and if so to what:
gears, _gears, __gears, __gears_, __gears__, ___gears
Recall Solution
Apply "two-front, one-back":
Written inside Widget |
Mangled? | Result |
|---|---|---|
gears |
No (zero leading) | gears |
_gears |
No (only one leading) | _gears |
__gears |
Yes | _Widget__gears |
__gears_ |
Yes (one trailing is allowed) | _Widget__gears_ |
__gears__ |
No (two trailing → dunder) | __gears__ |
___gears |
Yes (three leading still counts as "≥2") | _Widget___gears |
The last one surprises people: the rule says at least two leading underscores, so three qualifies. Mangling adds _Widget in front of the whole name including its underscores.
L1.2 — Name the mangled attribute
class Robot:
def __init__(self):
self.__battery = 100An outsider wants to read that value directly (without any getter). What exact attribute name must they type?
Recall Solution
__battery inside class Robot becomes _Robot__battery. So:
r = Robot()
print(r._Robot__battery) # 100Typing r.__battery fails with AttributeError, because that literal name was never stored — only the mangled one exists. This proves mangling is renaming, not locking.
Level 2 — Application
Goal: run the machinery yourself and predict real output.
L2.1 — Predict the output
class Vault:
def __init__(self):
self.__code = 4242
def check(self, guess):
return guess == self.__code
v = Vault()
print(v.check(4242))
print(v.__dict__)What do the two print lines produce?
Recall Solution
v.check(4242)— insidecheck,self.__codewas compiled asself._Vault__code, whose value is4242. So the comparison is4242 == 4242→True.v.__dict__— the instance dictionary stores the mangled key, so:
True
{'_Vault__code': 4242}There is no '__code' key anywhere. Whenever you're unsure what got stored, print __dict__ — it shows the real keys.
L2.2 — The guarded counter
class Counter:
def __init__(self):
self.__n = 0
def tick(self):
self.__n += 1
return self.__n
c = Counter()
c.tick()
c.tick()
c.__n = 999 # look carefully at this line
print(c.tick())
print(c.__dict__)What prints, and why doesn't c.__n = 999 change the count?
Recall Solution
Two tick() calls make _Counter__n go 0 → 1 → 2.
Now c.__n = 999 runs outside the class body, so no mangling happens there. It creates a brand-new attribute literally named __n — separate from _Counter__n. The real counter is untouched.
The next c.tick() uses self._Counter__n (currently 2), increments to 3, returns 3.
3
{'_Counter__n': 3, '__n': 999}Two different keys now coexist. This is the classic "I set it but nothing changed" confusion.
Level 3 — Analysis
Goal: reason about inheritance, where mangling earns its existence.
L3.1 — Whose __x wins?
class Base:
def __init__(self):
self.__x = "B"
def who(self):
return self.__x
class Sub(Base):
def __init__(self):
super().__init__()
self.__x = "S"
def who_sub(self):
return self.__x
s = Sub()
print(s.who())
print(s.who_sub())
print(sorted(s.__dict__.keys()))Predict all three lines.
Recall Solution
Each __x is mangled using the class it is written in:
Base.__init__stores_Base__x = "B";Base.whoreads_Base__x.Sub.__init__stores_Sub__x = "S";Sub.who_subreads_Sub__x.
They never collide. Both live in the same instance dict.
B
S
['_Base__x', '_Sub__x']who() reports "B" because it only ever looks at _Base__x. This is exactly the accident mangling prevents: without it, Sub's self.__x = "S" would have overwritten Base's field and broken who().
L3.2 — A single-underscore contrast
Rewrite L3.1 mentally with self._x (one underscore) everywhere instead of self.__x. Now what do the three lines print?
Recall Solution
One underscore means no mangling. Both classes write to the same key _x.
Base.__init__sets_x = "B".- Then
Sub.__init__(via its own line aftersuper().__init__()) sets_x = "S", overwriting it. - Both
who()andwho_sub()read the one shared_x, now"S".
S
S
['_x']Contrast with L3.1: the double underscore gave two independent slots (B and S); the single underscore gave one shared slot that the subclass clobbered. This side-by-side is the entire reason name mangling exists.
Level 4 — Synthesis
Goal: design a class that actually protects an invariant — mangling alone can't.
L4.1 — Build a guarded Speed class
Write a class Speed storing a speed in km/h that must satisfy 0 <= speed <= 300. Requirements:
- Reject invalid values with
ValueError, at construction and on later change. - Expose the value with clean
s.valuesyntax (noget_/set_). - Provide a read-only computed
mps(metres per second,value / 3.6).
Then predict:
s = Speed(72)
print(s.value, round(s.mps, 3))
s.value = 100
print(round(s.mps, 3))
# s.value = 400 -> ?
# s.mps = 10 -> ?Recall Solution
The Pythonic tool is @property (see Properties and Descriptors), backing the value in a _value internal:
class Speed:
def __init__(self, value):
self.value = value # goes through the setter's check
@property
def value(self):
return self._value
@value.setter
def value(self, v):
if not (0 <= v <= 300):
raise ValueError("speed out of range")
self._value = v
@property
def mps(self): # no setter -> read-only
return self._value / 3.6Why route __init__ through self.value = value instead of self._value = value? So construction uses the same validation as later assignment — one guard, no bypass. This is a class invariant enforced in one place.
Outputs:
Speed(72)→_value = 72;mps = 72 / 3.6 = 20.0. Prints72 20.0.s.value = 100passes the check;mps = 100 / 3.6 = 27.778. Prints27.778.s.value = 400→ setter sees400 > 300→ raisesValueError.s.mps = 10→mpshas no setter → raisesAttributeError.
Note: mangling was not the right tool here. It hides a name; it does not validate a value. Validation is the property's job.
Level 5 — Mastery
Goal: the degenerate and edge cases the parent note warned about.
L5.1 — Dunder immunity
class Model:
def __init__(self):
self.__init__called = True # careful: read the name exactly
def __str__(self):
return "a model"
m = Model()
print(str(m))
print('__str__' in dir(Model))
print(sorted(k for k in m.__dict__))Which of these names get mangled, and what prints?
Recall Solution
__init__and__str__end with two underscores → dunders → never mangled.str(m)calls the real__str__, printinga model, and'__str__' in dir(Model)isTrue.__init__calledis subtle: it has two leading underscores and no trailing double underscore (it ends ind). So it is mangled → stored as_Model__init__called.
a model
True
['_Model__init__called']The trailing test looks at the end of the whole identifier, not at an internal __. __init__called ends in d, so ≤1 trailing → mangled.
L5.2 — Mangling with a trailing underscore
class Node:
def __init__(self):
self.__next_ = None # one trailing underscore
n = Node()
print(sorted(n.__dict__))
print(hasattr(n, '_Node__next_'))Recall Solution
__next_ has two leading and exactly one trailing underscore → the rule allows ≤1 trailing → mangled → _Node__next_.
['_Node__next_']
TrueCompare __next__ (two trailing) which would not mangle. A single trailing underscore is a common trick to avoid clashing with a keyword (like class_), and it still gets mangled.
L5.3 — Mangling only fires inside a class body
class Bag:
def __init__(self):
self.__items = []
def add(self, x):
self.__items.append(x)
def sneaky(bag):
return bag.__items # this line is a plain function, not a class
b = Bag()
b.add("apple")
print(b.add.__self__ is b) # ignore this line's detail; it's True
try:
sneaky(b)
print("worked")
except AttributeError:
print("AttributeError")What prints for the sneaky call, and why?
Recall Solution
The function sneaky is defined at module level — not inside a class body — so bag.__items is not mangled. Python looks for a literal attribute __items on the instance, which doesn't exist (only _Bag__items does). Result: AttributeError.
True
AttributeErrorThis nails the mental model: mangling is a compile-time textual rewrite scoped to the enclosing class definition. Move the identical code outside the class and it stops mangling. To reach the field here you'd have to write the mangled name bag._Bag__items explicitly.
Recall Feynman recap: the one sentence
Name mangling is a name tag the interpreter staples on (_ClassName in front) to any __x-style name written inside a class, so different classes' fields never get mixed up — it is not a lock, not validation, and it doesn't fire outside class bodies.
Related builds: Access Modifiers (Java vs Python) · Abstraction — hiding complexity behind interfaces · Inheritance — base and derived classes