Encapsulation is the idea of bundling an object's data together with the methods that operate on it, and controlling how much of that data outside code is allowed to touch directly. In many languages this is enforced strictly with keywords like private or public. Python takes a different approach: it's largely convention-based, trusting you to respect the boundaries the author intended rather than locking them with hard restrictions.
Public Attributes (the Default)
By default, every attribute and method in Python is public — accessible from anywhere, with no special syntax.
class Account:
def __init__(self, balance):
self.balance = balance # public — anyone can read or change it
acc = Account(100)
acc.balance = 500 # allowed, nothing stops this
print(acc.balance)
Output:
500
Single Underscore: "Protected by Convention"
Prefixing an attribute with a single underscore (_balance) is a signal to other developers: "this is internal, don't touch it from outside the class," but Python does nothing to actually enforce it — it's purely a convention everyone agrees to respect.
class Account:
def __init__(self, balance):
self._balance = balance # convention: internal use only
def deposit(self, amount):
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance
acc = Account(100)
acc.deposit(50)
print(acc.get_balance())
print(acc._balance) # still technically accessible — just discouraged
Output:
150
150
Double Underscore: Name Mangling
A double-underscore prefix (__balance) triggers "name mangling" — Python internally renames the attribute to _ClassName__balance, which makes it awkward (though still not impossible) to access accidentally from outside the class. This is mainly useful to avoid naming clashes in subclasses, more than true privacy.
class Account:
def __init__(self, balance):
self.__balance = balance
def get_balance(self):
return self.__balance
acc = Account(100)
print(acc.get_balance())
print(acc._Account__balance) # the mangled name still works
Output:
100
100
The @property Alternative
Python's @property decorator lets you expose a method as if it were a plain attribute, which is the more idiomatic way to add controlled access (like validation on write) without forcing callers to use explicit getter/setter method calls. It's a deeper topic on its own, but worth knowing it exists once _single_underscore and __name_mangling start to feel limiting for real encapsulation needs.
This convention-based approach is part of why understanding classes and objects well matters in Python — the language gives you tools and trusts you to use them sensibly.
