Python · lesson 13 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Encapsulation 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.