Encapsulation and Name Mangling
Encapsulation is the practice of deciding what an object's state may be touched from outside and what must go through its methods. Python's stance is distinctive: there are no access modifiers in the Java sense, the language convention is a single leading underscore for "internal," and the double-underscore prefix triggers name mangling - a mechanical rename that makes accidental external access produce a visible error. The goal is not to make private truly impossible (in Python, nothing is), but to make the intended interface obvious and the accidental path loud.
The Single Underscore: Convention
class Engine:
def __init__(self, fuel):
self.fuel = fuel
self._rpm = 0 # convention: internal detail
def rev(self, rpm):
self._rpm = max(0, min(rpm, 8000))
def status(self):
return f"fuel={self.fuel:.0f} rpm={self._rpm}"
e = Engine(100)
e.rev(3000)
print(e.status()) # fuel=100 rpm=3000
print(e._rpm) # 3000 - readable from outside, by convention only
e._rpm = 99999 # also possible - nothing stops it
A single leading underscore (_rpm) is a promise to other developers, enforced by nothing: "this is an implementation detail; use the methods." Tools honor it - from module import * skips underscore names, and linters flag external access to them - but the runtime does not. That is the Python model: encapsulation as a documented contract, with the cost of a violation being a code-review comment rather than a compiler error.
Name Mangling: The Double Underscore
class Panel:
def __init__(self):
self.__value = 42 # double underscore: mangled
def get(self):
return self.__value # works: inside the class, un-mangled
p = Panel()
print(p.get()) # 42
print(p.__value) # AttributeError: 'Panel' has no attribute '__value'
print(p._Panel__value) # 42 - the mangled name, reachable if you insist
Inside the class body, any name of the form __name (two or more leading underscores, at most one trailing) is rewritten to _ClassName__name. So self.__value in Panel is stored as _Panel__value, and external code writing p.__value looks for the literal name __value, which does not exist. The effect is that subclasses accidentally reading a parent's double-underscore attribute get an error instead of silently using the wrong thing - which is exactly the collision mangling was designed to prevent. It is not privacy: the mangled name is public knowledge, and vars(p) shows it plainly.
Why Mangling Exists: The Subclass Collision
class Base:
def __init__(self):
self.__state = "base" # stored as _Base__state
class Child(Base):
def __init__(self):
super().__init__()
self.__state = "child" # stored as _Child__state - a DIFFERENT slot
c = Child()
print(vars(c))
# {'_Base__state': 'base', '_Child__state': 'child'} - both coexist
Without mangling, a subclass that defines its own self.__state would collide with the parent's, and a method in the parent reading self.__state would silently see the child's value (because name resolution would find the child's attribute). Mangling gives each class its own mangled slot, so the parent's "private" state cannot be clobbered by a same-named attribute in a subclass. That is the entire reason the mechanism exists - inheritance safety, not secrecy.
When Convention Is Not Enough
If an attribute must be protected by behavior - validated on set, computed on get - the answer is a property (chapter 14), which turns obj.x into a method call under the hood. The pattern: start with a plain attribute and a single-underscore convention, and promote to a property when a real rule appears (a range check, a cache, a unit conversion). This "start public, add a property when needed" path keeps the API clean without pre-emptively wrapping every attribute. True privacy in Python is a design statement, not a mechanism: the mechanism (mangling) only buys you the rename.
The Working Rules
- Public by default. An attribute with no underscore is part of the interface; treat renaming it as a breaking change.
_xfor internal state. Readable if you must, but the methods are the interface; linters will tell you when you cross the line.__xfor state that must survive subclassing. Use it in base classes whose internal storage a subclass might plausibly shadow; in leaf classes,_xis usually enough.- Never
__x__for your own names. The dunder pattern (double underscore both sides) is reserved for the special methods the protocol layer defines. - Prefer methods for rules. If setting
xrequires a check, a property (or a setter method) owns that check - the attribute alone cannot.
Summary
Python encapsulation is a convention plus one mechanism. The convention: public names are the interface, _name marks internal state, and the runtime lets you in anyway. The mechanism: __name is mangled to _ClassName__name, which is not private but is collision-proof against subclasses - the real reason it exists. Reserve the __x__ dunder shape for the protocol methods, and when an attribute needs rules on access, promote it to a property rather than fighting the language. The design posture: make the intended interface obvious, make the accidental path visible, and keep the actual enforcement in methods and properties where it is testable.