Python doesn’t have private or protected keywords like Java or C++. Instead, it relies on naming conventions and a gentle nudge called name mangling. The philosophy is “we’re all consenting adults here” — we trust developers to respect boundaries without the language forcing them.
The Three Levels
Single Underscore _name — “Protected”
A single leading underscore is a convention. It tells other developers “this is an internal implementation detail, please don’t use it directly.” But Python does absolutely nothing to stop anyone.
class BankAccount:
def __init__(self, balance):
self._balance = balance # "protected" — internal detail
def deposit(self, amount):
self._balance += amount
acc = BankAccount(100)
print(acc._balance) # 100 — works fine, just frowned upon
The only place Python enforces single underscores: from module import * won’t import names starting with _.
Double Underscore __name — Name Mangling
A double leading underscore triggers name mangling. Python rewrites the attribute name to _ClassName__name to avoid accidental name collisions in subclasses.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # name-mangled
acc = BankAccount(100)
# print(acc.__balance) # AttributeError!
print(acc._BankAccount__balance) # 100 — the mangled name
In simple language, Python doesn’t hide __balance — it just renames it. We can still access it if we really want to, but we have to go out of our way.
Why Name Mangling Exists
Name mangling isn’t about security. It’s about preventing accidental overwrites in subclasses. Here’s the actual use case:
class Base:
def __init__(self):
self.__value = 10 # becomes _Base__value
class Child(Base):
def __init__(self):
super().__init__()
self.__value = 20 # becomes _Child__value (different name!)
c = Child()
print(c._Base__value) # 10 — Base's version is safe
print(c._Child__value) # 20 — Child's version is separate
Without name mangling, Child.__value would have overwritten Base.__value. The mangling keeps them separate.
Dunder Methods Are NOT Mangled
A common misconception: names with double underscores on both sides (like __init__, __str__) are not mangled. Name mangling only applies to names with two or more leading underscores and at most one trailing underscore.
class Foo:
def __init__(self): # NOT mangled — dunder method
self.__x = 1 # mangled to _Foo__x
self.__y__ = 2 # NOT mangled — has trailing underscores
Using @property for Real Encapsulation
The Pythonic way to control access is through properties. We keep the attribute “private” and expose it through a clean interface.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # single underscore convention
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
t = Temperature(25)
print(t.celsius) # 25 — looks like attribute access
t.celsius = 30 # validation runs automatically
# t.celsius = -300 # ValueError: Below absolute zero!
This is the preferred approach. We get the clean syntax of attribute access (t.celsius) with the power of validation behind the scenes.
When to Use What
- No underscore (
self.name): public API, meant for external use. - Single underscore (
self._name): internal detail. “Don’t use this unless you know what you’re doing.” - Double underscore (
self.__name): use only when we specifically need to avoid name collisions in a deep inheritance hierarchy. It’s rare. @property: when we need validation, computed values, or want to make an attribute read-only. This is the Pythonic encapsulation tool.
In simple language, Python trusts us to respect naming conventions instead of enforcing strict access control. Single underscore means “internal,” double underscore prevents accidental name clashes in subclasses, and @property is how we build real controlled access when we need it.