Skip to main content
Python · lesson 13 of 34
Python beginner Lesson 13 of 34

Encapsulation and Name Mangling in Python

Learn how Python encapsulates state with the underscore convention and name mangling, and why neither one is real access control.

By SkillByExample · Updated

Python doesn’t enforce private/protected/public the way Java or C++ do. Instead it relies on naming conventions the language partially backs with a rewriting trick called name mangling. Output below is from Python 3.13.

The Underscore Conventions

A single leading underscore signals “treat this as internal,” but it’s just a hint — Python does nothing to stop you reading it. A double leading underscore triggers name mangling, which rewrites the attribute name at class-definition time.

class Account:
    def __init__(self, owner, balance):
        self.owner = owner            # public - no restriction
        self._balance = balance       # single underscore - "protected by convention" only
        self.__pin = "1234"           # double underscore - name-mangled

    def show_pin_internally(self):
        return self.__pin             # works fine from inside the class

acc = Account("Priya", 500)
print(acc.owner)
print(acc._balance)
print(acc.show_pin_internally())

try:
    print(acc.__pin)
except AttributeError as e:
    print(f"AttributeError: {e}")

print(acc._Account__pin)
$ python encapsulation.py
Priya
500
1234
AttributeError: 'Account' object has no attribute '__pin'
1234

acc.__pin fails because Python silently rewrote self.__pin inside the class body to self._Account__pin — that’s the actual attribute name stored on the instance. acc.__pin looks for a literal __pin attribute that was never created. The last line proves the “protection” is really just a rename: knowing the mangled name gets you in anyway.

Why Name Mangling Actually Exists

The real purpose isn’t hiding data from callers — it’s preventing a subclass from accidentally clobbering a parent class’s internal attribute of the same name.

class Base:
    def __init__(self):
        self.__data = "base secret"   # mangled to _Base__data

    def reveal_base(self):
        return self.__data            # always resolves to _Base__data, even in a subclass

class Child(Base):
    def __init__(self):
        super().__init__()
        self.__data = "child secret"  # mangled to _Child__data - a DIFFERENT attribute

    def reveal_child(self):
        return self.__data            # resolves to _Child__data

c = Child()
print(c.reveal_base())
print(c.reveal_child())
print(sorted(k for k in c.__dict__ if "data" in k))
$ python subclass_mangling.py
base secret
child secret
['_Base__data', '_Child__data']

Both classes write self.__data, but because mangling includes the defining class’s name, Base and Child end up with two independent attributes, _Base__data and _Child__data. Without mangling, Child.__init__ would silently overwrite Base’s attribute — a bug that would only show up when reveal_base() returned the wrong value.

Practice

1. Add a __log list to Account that records every deposit, and a history() method that returns it. Confirm the mangled name shows up in acc.__dict__.

Solution
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance
        self.__log = []

    def deposit(self, amount):
        self._balance += amount
        self.__log.append(amount)

    def history(self):
        return self.__log

acc = Account("Priya", 500)
acc.deposit(100)
acc.deposit(25)
print(acc.history())
print([k for k in acc.__dict__ if "log" in k])
$ python deposit_log.py
[100, 25]
['_Account__log']

Inside the class, self.__log is written as self._Account__log, and that rewritten name is the key stored in the instance dictionary.

2. A subclass wants to change a parent’s __mode. Show that assigning self.__mode in the subclass does not affect the parent’s method, and that only the explicit mangled name does.

Solution
class Base:
    def __init__(self):
        self.__mode = "base"

    def mode(self):
        return self.__mode          # always reads _Base__mode

class Child(Base):
    def __init__(self):
        super().__init__()
        self.__mode = "child"       # creates _Child__mode, a separate attribute

c = Child()
print(c.mode())
c._Base__mode = "forced"            # targeting the mangled name directly
print(c.mode())
$ python override_mangled.py
base
forced

The subclass’s self.__mode becomes _Child__mode, so Base.mode() never sees it. Reaching in with _Base__mode works, but writing another class’s mangled name is a clear signal you are breaking its encapsulation on purpose.

3. Predict, then verify: does name mangling apply to a double-underscore attribute assigned outside a class body?

Solution
class Account:
    pass

acc = Account()
acc.__pin = "9999"          # assigned at module level: no mangling
print(acc.__dict__)
print(acc.__pin)
$ python outside_mangling.py
{'__pin': '9999'}
9999

Mangling is done by the compiler for names written inside a class block. Code at module level is not in a class, so __pin is stored and read back under its literal name.

4. Refactor Account so _balance is exposed through a read-only @property called balance, while deposit remains the only way to change it.

Solution
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        self._balance += amount

acc = Account(100)
acc.deposit(50)
print(acc.balance)
try:
    acc.balance = 500
except AttributeError as e:
    print(f"AttributeError: {e}")
$ python readonly_balance.py
150
AttributeError: property 'balance' of 'Account' object has no setter

A property with no setter rejects assignment, so callers can read balance but only change it through deposit(), where you can add validation later.

Frequently Asked Questions

Does Python have private variables like Java or C++?
Not enforced ones. A single leading underscore (_name) is a convention meaning "internal use only," and a double leading underscore (__name) triggers name mangling, which just renames the attribute — it doesn't block access.
What is name mangling in Python?
When you write self.__attr inside a class body, Python rewrites it internally to self._ClassName__attr. This mostly exists to stop a subclass from accidentally overwriting a parent class's internal attribute, not to hide data from outside callers.
Can I still access a double-underscore attribute from outside the class?
Yes, by using the mangled name directly, e.g. obj._ClassName__attr. Nothing in the language raises a permission error — it's obfuscation, not enforcement.
When should I use a single underscore versus a double underscore?
Default to a single underscore (_name) for internal attributes — it communicates intent clearly. Reach for a double underscore (__name) only when you specifically need to avoid a naming collision in a subclass, which is rare.