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.