Encapsulation & Name Mangling

intermediate encapsulation name-mangling private protected access-control

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

Python Access Levels
name
Public
Access from anywhere. No restrictions.
_name
Protected (by convention)
A signal that says "internal use, don't touch." Not enforced.
__name
Name-mangled
Python renames it to _ClassName__name. Harder to access accidentally.

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.