Encapsulation
Encapsulation is the idea of bundling data and the methods that operate on that data together inside a class, while restricting direct access to some of that data from outside the class. It's meant to protect an object's internal state from being changed in ways that could leave it inconsistent or broken. In languages like Java and C++, encapsulation is enforced by the compiler using keywords like private and protected. Python takes a very different approach.
The Single Underscore Convention: `_var`
Prefixing an attribute or method name with a single underscore, like _balance, is a universally understood signal in the Python community: "this is internal — treat it as an implementation detail, not part of the public API." It's a convention only. Python itself does nothing special with it; the attribute is still perfectly accessible from outside the class.
class BankAccount:
def __init__(self, balance):
self._balance = balance # "internal use" by convention
def deposit(self, amount):
self._balance += amount
account = BankAccount(100)
account.deposit(50)
print(account._balance) # works fine — Python doesn't stop you
account._balance = -9999 # also works fine, even though it's a bad idea150
Nothing prevented either of those last two lines from running. The single underscore is a note to other developers (and to tools like linters and IDEs, which will often warn about accessing it) — not a technical barrier.
Name Mangling: `__var`
A double leading underscore, like __var, triggers something Python actually does rewrite: name mangling. When Python sees self.__var inside a class body, it internally renames that attribute to self._ClassName__var, where ClassName is the name of the class it was defined in. This makes the attribute harder to stumble upon accidentally from outside, but it is still fully reachable if you know (or can discover) the mangled name.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # gets mangled
def get_balance(self):
return self.__balance
account = BankAccount(100)
print(account.get_balance()) # 100
# print(account.__balance) # would raise AttributeError
print(account._BankAccount__balance) # this works — proving it's obfuscation, not privacy100 100
That last line is the whole point: __balance was never made private, it was just renamed to _BankAccount__balance. Anyone who knows Python's mangling rule (or simply inspects the object with dir()) can access it directly from outside the class.
"We're All Consenting Adults Here"
This design reflects a well-known piece of Python philosophy sometimes summarized as "we are all consenting adults here." Rather than the language locking away internal state and forcing you through a strict, compiler-enforced gate, Python trusts developers to respect naming conventions and documented boundaries. It optimizes for flexibility, introspection, and debuggability — being able to peek at (or patch) an object's internals when you truly need to, such as during testing or debugging — over strict, unbreakable enforcement.
Contrast this with Java or C++, where marking a field private is a hard rule checked by the compiler: code outside the class simply will not compile if it tries to touch that field directly. Python's underscores are social contracts backed by very little technical enforcement, verified only at your own discretion.
public_var— fully public, no restriction, no special treatment._protected_var— internal by convention only; accessible, but you are expected not to touch it from outside.__private_var— name-mangled to_ClassName__private_var; harder to reach by accident, still reachable on purpose.
A Better Way: `@property`
Underscored attributes control visibility by convention, but they don't let you add validation, computed values, or side effects when an attribute is read or written — an object can still be handed an invalid value directly. Python's Pythonic answer to that is the @property decorator, which lets a method be accessed with plain attribute syntax (account.balance instead of account.get_balance()) while still running real code behind the scenes, including validation on assignment. That's the proper tool for controlled access in Python, and it's covered in full on the "Properties & Getters/Setters" page.