Python encapsulation works on trust, not locks. A single underscore says “please leave this alone,” a double underscore makes the name harder to reach, and that is the whole system. No compiler stands guard. This post shows how public, protected, and private attributes really behave, what name mangling does, and the Pythonic @property pattern that gives you safe, validated access.
“In Python, we’re all consenting adults here.”
Alex Martelli, Python in a Nutshell
Think of a shared office fridge. Your coworker Rahul has stuck a label on his shelf that says “Rahul, hands off.” The shelf is not locked. Anyone can open the fridge and take his sandwich. The label is a social signal, not a barrier. That is exactly how Python encapsulation works. The underscores in front of an attribute name are sticky notes that say “internal, please don’t touch,” and a polite team respects them. Python itself will not stop a determined person.
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 14 minutes
Prerequisites: You should be comfortable with Classes & Objects, Inheritance, and Polymorphism. This post builds directly on object-oriented programming (OOP) fundamentals.
If you come from Java or C++, this will feel strange at first. Those languages have private, protected, and public keywords, and the compiler refuses to build your code if you break the rules. Python has none of that. It trusts you and your teammates to follow conventions. The payoff is less ceremony. The cost is that “private” in Python is a promise, not a wall.
Table of Contents
Public Attributes: No Restrictions
Before any code, here is the mental model. Python has three levels of access, and the diagram below shows what each one allows from outside code. Public attributes are wide open, like a notice board in the office lobby: anyone can read it, and anyone can pin something new on it. A single underscore is a polite request. A double underscore gets renamed behind the scenes so a casual obj.__name stops working. Notice that none of these are real locks: the door always opens if you push hard enough.
The diagram contrasts Python’s three access levels. Public attributes have no underscore and work from anywhere. Protected attributes have a single underscore, which is a convention that means “internal use, please leave it alone.” Private attributes have a double underscore, which Python renames (mangles) so a subclass cannot overwrite it by accident. The key idea: in Python this is about convention, not enforcement. The single underscore is a request, the double underscore is name mangling, and the code below proves both.
📄 public_attrs.py: the default, no underscores
class User:
def __init__(self, name, email):
self.name = name # Public
self.email = email # Public
user = User("Rahul", "rahul@technoscripts.com")
print(user.name) # Read it from anywhere
user.name = "Aditi" # Change it from anywhere
print(user.name)
▶ Output
Rahul Aditi
What happened here: No underscores, no rules. The name attribute is public, so you can read it and overwrite it from anywhere in your program. This is where you start every class. Most attributes stay public for their whole life, and that is completely fine. You only reach for underscores when an attribute is genuinely internal and you want to warn other developers off it.
Protected Attributes: The Single Underscore
One underscore in front of a name, like _balance, is Python’s “staff only” sign. It tells anyone reading your code that this attribute is internal plumbing and they should use the public methods instead. Here is the catch: Python does not enforce it at all. The sign hangs on a door with no lock.
📄 protected_attrs.py: the “please do not touch” convention
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # Protected by convention only!
def deposit(self, amount):
if amount > 0:
self._balance += amount
return self._balance
def get_balance(self):
return self._balance
account = BankAccount("Vinay", 10000)
print(account.get_balance())
# This WORKS. Python does not enforce the underscore.
print(account._balance) # No error, just a convention you broke
account._balance = 999999 # You CAN do this. You SHOULDN'T.
print(account._balance)
▶ Output
10000 10000 999999
What happened here: The output reads 10000, then 10000, then 999999. The first line came from the proper interface, get_balance(). The second line read account._balance directly, ignoring the convention, and Python allowed it without a murmur. The last line proves the point: we reached straight into account._balance and set it to 999999, and Python did not blink. The single underscore is a sign, not a lock. A good teammate reads the sign and uses deposit() instead. Python will never force the issue.
Private Attributes: Name Mangling
Two underscores in front of a name turn on a feature called name mangling. Python quietly rewrites __balance to _ClassName__balance behind your back. Now a plain account.__balance fails, because that exact name does not exist anymore. It is less like a lock and more like filing your sandwich in the fridge under a long, awkward code name. People can still find it, but not by accident.
📄 name_mangling.py: a double underscore triggers name mangling
class SecureAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Name mangled!
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return self.__balance
account = SecureAccount("Anvay", 5000)
print(account.get_balance())
# Direct access fails
try:
print(account.__balance)
except AttributeError as e:
print(f"Error: {e}")
# But name mangling is NOT security. You can still reach it
# if you know the mangled name.
print(account._SecureAccount__balance) # Works!
# The mangled name is sitting right there in plain sight
print([a for a in dir(account) if a.endswith("__balance")])
▶ Output
5000 Error: 'SecureAccount' object has no attribute '__balance' 5000 ['_SecureAccount__balance']
What happened here: Python mangled __balance into _SecureAccount__balance. So account.__balance raises AttributeError, because that name was never stored. But account._SecureAccount__balance works fine, and the last line shows the mangled name openly listed by dir(). The takeaway: name mangling is not security. It exists to stop accidental name collisions between a parent class and its subclasses, which is exactly what the next example shows.
Why Name Mangling Exists
Why would Python rename your attributes at all? Think of two flatmates who both label a kitchen jar “Sugar.” Sooner or later one of them refills the wrong jar. The fix is to relabel them “Anvi’s Sugar” and “Aviraj’s Sugar,” and now there is no mix-up. Name mangling does the same relabelling for classes. Picture a parent class and a child class that both, without knowing about each other, store an attribute called __secret. Without mangling, the child’s value would silently clobber the parent’s, and a method on the parent would suddenly read the wrong data. Name mangling keeps the two apart by giving each one a class-specific name.
📄 mangling_purpose.py: stopping an accidental override in inheritance
class Parent:
def __init__(self):
self.__secret = "parent's secret"
def get_secret(self):
return self.__secret
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = "child's secret" # Different mangled name!
def get_child_secret(self):
return self.__secret
obj = Child()
print(obj.get_secret()) # Parent's version
print(obj.get_child_secret()) # Child's version
# They're stored as different attributes
print(obj._Parent__secret) # parent's secret
print(obj._Child__secret) # child's secret
▶ Output
parent's secret child's secret parent's secret child's secret
What happened here: Both Parent and Child define __secret, but mangling stored them under different real names: _Parent__secret and _Child__secret. So the parent’s method still reads the parent’s secret, and the child’s method reads the child’s. Without mangling, the child would have quietly overwritten the parent’s attribute and broken it. That is the whole reason name mangling exists: it prevents collisions in class hierarchies. It was never meant to hide data from a programmer who is determined to find it.
The Pythonic Encapsulation Pattern
So if underscores are just signs, how do you actually protect your data, for example to reject a temperature below absolute zero? You use @property. It lets the caller write t.celsius = 100 with clean, normal syntax, while you run a validation check behind that simple assignment. Think of it like a polite receptionist sitting in front of the value: visitors walk up and ask normally, and the receptionist quietly checks their request before letting it through.
📄 pythonic_encapsulation.py: the property-based approach
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # Protected by 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
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
t = Temperature(25)
print(f"{t.celsius}C = {t.fahrenheit}F")
t.celsius = 100
print(f"{t.celsius}C = {t.fahrenheit}F")
try:
t.celsius = -300
except ValueError as e:
print(f"Error: {e}")
▶ Output
25C = 77.0F 100C = 212.0F Error: Below absolute zero!
What happened here: This is encapsulation done the Python way. The real value lives in _celsius, protected by convention. The @property turns celsius into a smart attribute: reading it runs the getter, assigning it runs the setter, and the setter rejects anything below absolute zero with a clear ValueError. The caller never sees any of this machinery. They just write t.celsius = 100 like a normal attribute, and fahrenheit is computed on the fly, so it can never drift out of sync. You get validation and computed values with zero ugly get_x() or set_x() calls. There is much more on this in the property decorators tutorial.
The Catch: Double Underscore Is Not Privacy
This is the one thing nearly everyone gets wrong about Python encapsulation. People see the double underscore, remember private from Java, and assume the data is locked away. It is not. A double underscore is like a hotel safe with the combination printed on the door: it stops nobody who bothers to read. Watch this happen live in the REPL (Read-Eval-Print Loop), where you can see each line answered as you type it.
📄 Python REPL: a “private” pin you can still read
>>> class Vault:
... def __init__(self):
... self.__pin = "1234" # looks private, right?
...
>>> v = Vault()
>>> v.__pin # the obvious way fails
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
v.__pin # the obvious way fails
^^^^^^^
AttributeError: 'Vault' object has no attribute '__pin'
>>> v._Vault__pin # the mangled name works fine
'1234'
What happened here: The pin was never hidden. Python just stored it under the mangled name _Vault__pin, and anyone who knows the class name can read it in one line. So treat the double underscore as collision protection, not as a safe. If you have a real secret like a password or an API (Application Programming Interface) key, encapsulation will not protect it. Hash it, encrypt it, or keep it out of the object entirely.
When You Will Use This
Python encapsulation is not abstract theory. You will reach for it in everyday code. Here are three moments where it earns its keep.
- Validating user input on the way in. When you build a class that holds an age, a price, or a temperature, wrap the value in a
@propertysetter so a bad value is rejected at the source. Say a developer named Niranjan builds a checkout class: his setter can refuse a negative quantity before it ever reaches the database. - Marking internal helpers in a library. When you write a module other people import, prefix the bits that are not part of the public API with a single underscore. It tells users “build against the documented methods, not against
_cache, because_cachemay change tomorrow.” - Exposing a computed value as if it were stored. A
@propertywith no setter is perfect for things likefahrenheitor a full name built from first and last. The caller readsperson.full_namelike a plain attribute, and it can never fall out of sync because it is recomputed every time.
Common Mistakes
Mistake 1: Using double underscore for security
Name mangling is not encryption. Anyone who knows the class name can read _ClassName__attr in a single line, as the catch above showed. If you need real security, hash the value, encrypt it, or keep it out of the object. Use the double underscore for what it is good at: stopping accidental name clashes in a class hierarchy.
Mistake 2: Java-style getters and setters everywhere
📄 mistake_java_style.py
# BAD: Java style, not Pythonic
class UserBad:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
# GOOD: Pythonic. Start simple, add @property later if you ever need it.
class UserGood:
def __init__(self, name):
self.name = name # Just use a public attribute!
Coming from Java, the instinct is to wrap every attribute in get_name() and set_name() on day one, just in case. In Python that is wasted code. Start with a plain public attribute. If you later need validation, swap in a @property and the caller’s code (user.name) does not change one character. That invisible switch is Python’s uniform access principle, and it is why the boilerplate getters are pure noise.
Best Practices
- DO start with public attributes and add protection later only if you need it
- DO use a single underscore
_attrfor internal-use attributes - DO use
@propertywhen you need validation or a computed value - DON’T reach for a double underscore
__attrhoping for “privacy.” Use it to prevent inheritance collisions, nothing more - DON’T write Java-style
get_x()andset_x()methods. Reach for@propertyinstead
Conclusion
Python encapsulation runs on convention, not enforcement. A single underscore says “internal, please leave it alone.” A double underscore triggers name mangling, which prevents accidental name collisions between a class and its subclasses. Neither one is a real lock, so do not lean on them for security. The Pythonic move is to start with public attributes and reach for a @property only when you actually need validation or a computed value.
Python encapsulation protects your data. Abstract classes define your contracts. In the abstract classes tutorial, you will learn how to build base classes that force subclasses to implement specific methods. That is the foundation of plugin architectures and framework design. And if you want the full learning path from basics to machine learning, browse the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Build a
Studentclass that stores a_scorewith a single underscore, then read it from outside the class to prove Python does not stop you. - Exercise 2: Write a
BankAccountwith a@propertyforbalancewhose setter raisesValueErrorif anyone tries to set a negative amount. - Exercise 3: Give a parent and a child class an attribute called
__ideach, then print_Parent__idand_Child__idto watch name mangling keep them apart.
Frequently Asked Questions
What is encapsulation in Python?
Python encapsulation means bundling data and methods together and controlling access to the internal state. Python does this through naming conventions (a single underscore for protected, a double underscore for name-mangled) and @property decorators, not through compiler keywords like Java’s private.
What is the difference between _ and __ in Python class attributes?
A single underscore _attr is a convention meaning ‘internal use, please do not access from outside.’ A double underscore __attr triggers name mangling, so Python renames it to _ClassName__attr to prevent accidental override in subclasses. Neither one is truly private.
What is name mangling in Python?
When you prefix an attribute with a double underscore (__attr), Python internally renames it to _ClassName__attr. This stops child classes from accidentally overwriting a parent attribute that has the same name. It is a collision-prevention mechanism, not a security feature.
Does Python have private variables?
Not in the Java or C++ sense. Python has no access modifiers enforced by the compiler. A double underscore triggers name mangling, which makes direct access harder but not impossible. Python’s philosophy is ‘we are all consenting adults here,’ so it favours conventions over enforcement.
Should I use getters and setters in Python?
Not Java-style get_name() and set_name(). Use @property instead. Start with public attributes, and if you later need validation, add a @property so the calling code (obj.name) does not change. That is Python’s uniform access principle.
Interview Questions on Python Encapsulation
These come from real screens and onsites. Practice answering before you read each answer.
Q: A teammate stores an API key as self.__api_key and says it is safe because the attribute is private. What do you tell them?
It is not safe. The double underscore only triggers name mangling, so the key is sitting in the object as _ClassName__api_key, readable in one line and visible in dir() and __dict__. Name mangling prevents attribute name collisions in inheritance, nothing more. Real secrets belong in environment variables or a secrets manager, and passwords should be hashed, not stored on the object.
Q: What exactly does Python do with a name like __balance inside a class body?
At compile time, any identifier with two leading underscores and at most one trailing underscore that appears inside a class body is textually rewritten to _ClassName__balance. This applies everywhere in the class body, including method code, not just to self attributes. Names with two trailing underscores, like __init__, are exempt, which is why dunder methods are never mangled.
Q: You shipped a class with a public attribute price, and now you need to reject negative values. Hundreds of files already write item.price = x. How do you add validation without breaking them?
Rename the stored value to _price and add a @property getter plus a @price.setter that raises ValueError for negative input. Callers keep writing item.price = x with zero changes, because property access looks identical to plain attribute access. This is Python’s uniform access principle, and it is the reason you should not write defensive getters and setters on day one.
Q: A bug report shows a Temperature object holding -500 even though your @celsius.setter raises ValueError below -273.15. How is that possible, and what do you check first?
The setter only runs when code assigns through the property name. Somewhere, code is writing to the backing attribute directly, either t._celsius = -500 or t.__dict__["_celsius"] = -500, and both skip validation entirely. Search the codebase for writes to _celsius outside the class. This is also a reminder that the single underscore is a convention, so code review has to enforce what the language will not.
Q: When would you actually choose a double underscore over a single underscore?
Almost never, and that is the honest answer interviewers want. Default to a single underscore for internal attributes. Reach for a double underscore only when you are writing a base class designed for wide subclassing and you need to guarantee that a subclass defining the same attribute name cannot clobber yours. Using it as a privacy tool signals a Java habit, not a Python one.
Q: Does name mangling work on methods too, or only on attributes?
Methods too. A method named __helper becomes _ClassName__helper, so obj.__helper() fails from outside while calls inside the class body still work, because those call sites are mangled the same way. The rule is about identifiers in a class body, so it covers methods, attributes, and even references to global names that happen to start with two underscores.
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: Python: Polymorphism, Method Overriding & Duck Typing
Next: Python: Abstract Classes & Interfaces (ABC Module)
Series Home: Python + AI/ML Tutorial Series

No comment