The Story That Explains Private Members
The bulletin board in the hallway — pinned notes about the rubbish schedule, the WiFi password, the address. Anyone can read them and update them. These are public attributes.
Your bedroom door with a "please knock" sign — the door isn't locked. Your flatmates could walk in, but by convention they don't. If they enter anyway, it's their responsibility if something breaks. These are protected attributes (single underscore).
Your locked personal safe inside the bedroom — passport, bank cards, diary. The house rules actively rename the safe to make it hard to find by accident. You can technically still crack it open, but you've clearly gone out of your way. These are private attributes (double underscore, name-mangled).
Python doesn't have a hard
private keyword. It uses naming conventions
and one small piece of magic (name mangling) to signal intent: "public — help yourself,"
"protected — don't unless you know what you're doing," and "private — really, this is not for you."
Python doesn't enforce privacy the way Java or C++ does — it signals it.
A single underscore _x is a polite request. A double underscore __x
triggers name mangling, which renames the attribute internally to make
accidental access very unlikely. Together they let you build classes with a clear, safe
public API and an internal implementation that outsiders can't casually touch.
The Problem — Python Makes Everything Public by Default
When you write self.balance = 1000 in __init__, any caller can
reach into that object and change account.balance = -1_000_000. Nothing stops
them. That's fine for a script, but for a real class it's a disaster.
class BankAccount:
def __init__(self, holder, balance):
self.holder = holder
self.balance = balance # public — anyone can touch it
acc = BankAccount("Alice", 1000)
# Nothing stops this — the whole class contract is gone
acc.balance = -99999999
print(acc.balance) # -99999999 ← no validation, no history, no protection
Python's philosophy is "we are all consenting adults." It gives you tools to signal what's part of your public API and what isn't. Callers who ignore those signals do so at their own risk. That's usually enough — because in real teams, code review catches anyone who tries to reach into a class's private innards.
Single Underscore _protected — "Please Knock"
A name that starts with one underscore (self._log,
self._retries) is a hint to every reader: "this is internal to the class.
Don't touch it from outside unless you know what you're doing." Python itself does nothing
special — no mangling, no access errors. It's a pure convention, universally respected in the
community.
class BankAccount:
def __init__(self, holder, balance):
self.holder = holder # public — okay to read/write
self._log = [] # protected — internal use, but subclasses can extend
self._version = 1 # protected — for internal bookkeeping
acc = BankAccount("Alice", 1000)
# You CAN still touch _log from outside — Python doesn't stop you...
print(acc._log) # [] (works — but you shouldn't)
# ...but every Python developer reads "_log" as "don't touch, this is internal."
Use _name for attributes and methods that are internal but
subclasses may still need them. Common examples: _log,
_cache, _state, _normalize(). The single underscore
says "not part of my public API, but not sealed off from my family either."
Double Underscore __private — Name Mangling Locks the Safe
A name that starts with two underscores (and doesn't end in two) triggers
Python's name mangling. Python quietly renames self.__balance
behind the scenes to self._ClassName__balance. Anyone who tries the obvious
account.__balance from outside gets an AttributeError.
class BankAccount:
def __init__(self, holder, balance):
self.holder = holder
self.__balance = balance # private — will be renamed by Python
self.__pin = None # private — you REALLY can't leak this
def show(self):
# Inside the class, __balance works as expected
print(f"{self.holder}: ${self.__balance}")
acc = BankAccount("Alice", 1000)
acc.show() # Alice: $1000
# ── The obvious access from outside FAILS ──
try:
print(acc.__balance)
except AttributeError as e:
print("Blocked:", e) # 'BankAccount' object has no attribute '__balance'
# ── What Python actually did: it renamed the attribute ──
print(acc.__dict__) # {'holder': 'Alice', '_BankAccount__balance': 1000, '_BankAccount__pin': None}
# ── You COULD still reach it via the mangled name, but everyone knows you're cheating ──
print(acc._BankAccount__balance) # 1000 — works, but any reviewer will reject this
Name mangling stops accidental access — the "oops, I typed
acc.__balance" mistake. It does not stop determined attackers.
Anyone who wants can still write acc._BankAccount__balance. Private in Python
means "actively discouraged," not "cryptographically sealed." Never use it as a security
boundary.
Animated Diagram — Public vs Protected vs Private Access
Three attributes on one BankAccount. Watch what happens when outside code tries
to reach each of them. Public wins instantly, protected works but with a "you shouldn't" warning,
private crashes with an AttributeError.
Three attempts, three different outcomes. Public is open, protected is a signal, private is actually harder to reach because Python renamed it.
Animated Diagram — How Name Mangling Works
When Python compiles the class body, any name starting with __ (and not ending in
__) is silently rewritten. The rule is simple: prepend _ClassName.
Inside the class, self.__balance and the mangled name behave identically. From
outside, only the mangled name works.
Name mangling is deterministic: __name in class C always becomes _C__name. Once you know the rule, no mystery — just an active nudge to leave it alone.
__dunder__ Is Not Private
Names with double underscores on both ends — __init__,
__str__, __len__ — are called dunders ("double
underscore"). They are not mangled and are not private. Python reserves
them for special protocol methods that you're expected to override. Only names that start
with __ but don't end with __ get mangled.
Private Methods — Helpers That Should Never Be Called From Outside
Everything you learned about private variables applies to private methods
identically. A method whose name starts with __ gets name-mangled just like an
attribute. Use this to hide implementation details — validation, formatting, internal setup —
that callers should never care about.
class User:
def __init__(self, username, password):
self.username = username
self.__password_hash = self.__hash(password) # uses private method
# ── Public API ─────────────────────────────────────
def check_password(self, guess):
return self.__password_hash == self.__hash(guess)
# ── Private helper — never called from outside ─────
def __hash(self, s):
# In real code: hashlib.sha256(...). Toy here.
return hex(hash(s))
alice = User("alice", "secret")
print(alice.check_password("secret")) # True (public method works)
print(alice.check_password("guess")) # False
# Calling the private helper from outside → AttributeError
try:
alice.__hash("anything")
except AttributeError as e:
print("Blocked:", e)
Animated Diagram — Public API Calls Private Helpers
Outside code only ever calls the public method withdraw(500, pin). Behind that
door, the method invokes several private helpers — __verify(),
__log_txn() — that the caller never sees or knows about. That's encapsulation.
Callers see one clean entrance. Behind that door, private helpers do the real work — verify, check, log. The caller never sees them, never depends on them.
Practical Example — BankAccount With Private Balance and PIN
A realistic small class that uses all three access levels together: a public API, protected logging that subclasses may extend, and truly private state (balance, PIN) that only the class itself touches through private helper methods.
class BankAccount:
def __init__(self, holder, opening_balance, pin):
if not BankAccount.__is_valid_pin(pin):
raise ValueError("PIN must be 4 digits")
self.holder = holder # public — the customer's name
self._log = [] # protected — subclasses may extend
self.__balance = opening_balance # private — the whole point of a bank
self.__pin = pin # private — should NEVER leak
# ══ PUBLIC API — the only entry points ═══════════════════
def deposit(self, amount, pin):
self.__verify(pin)
if amount <= 0:
raise ValueError("Amount must be positive")
self.__balance += amount
self.__log_txn("deposit", amount)
def withdraw(self, amount, pin):
self.__verify(pin)
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
self.__log_txn("withdraw", amount)
def get_balance(self, pin):
# Even reading the balance requires the PIN
self.__verify(pin)
return self.__balance
# ══ PRIVATE HELPERS — hidden implementation details ══════
def __verify(self, pin):
if pin != self.__pin:
raise PermissionError("Wrong PIN")
def __log_txn(self, action, amount):
self._log.append((action, amount))
@staticmethod
def __is_valid_pin(pin):
return isinstance(pin, str) and len(pin) == 4 and pin.isdigit()
# ── Normal use — everything works through the public API ───
acc = BankAccount("Alice", opening_balance=1000, pin="1234")
acc.deposit(500, pin="1234")
acc.withdraw(200, pin="1234")
print("Balance:", acc.get_balance(pin="1234")) # Balance: 1300
print("Log: ", acc._log) # protected — visible but internal
# ── Attempts to bypass the API ─────────────────────────────
try: acc.withdraw(100, pin="9999")
except PermissionError as e: print("Blocked:", e) # wrong PIN
try: print(acc.__balance)
except AttributeError as e: print("Blocked:", e) # direct read fails
try: acc.__verify("9999")
except AttributeError as e: print("Blocked:", e) # private helper is not callable
Nobody can set the balance directly — every mutation goes through deposit
or withdraw, which validate. Nobody can read the balance without the PIN.
Nobody can call __verify to fish for correct PINs. The class exposes a small,
safe surface, and everything else is hidden behind name mangling. That's
encapsulation in Python.
Public API vs Private Implementation — The Contract
Named with __x (double underscore prefix) |
Python renames them → _ClassName__x |
| You are free to change these any time without warning |
| Callers must not depend on them |
Examples: __balance, __pin, __hash() |
Named with no prefix (deposit, get_balance) |
| Callable from anywhere — that's the point |
| You promise not to break these without a version bump |
| Every caller safely depends on them |
| Change them → everyone downstream needs to update |
The whole point of marking members private is freedom. Once you've committed to a public method, breaking its signature means breaking every caller. But you're free to swap out the private implementation whenever — add caching, change algorithms, split a helper into three — as long as the public API keeps behaving the same way. Private members are the space where you can refactor without fear.
Common Mistakes (and Fixes)
| Mistake | What Goes Wrong | Fix |
|---|---|---|
| Reading a private attribute from outside via mangling | acc._BankAccount__balance works, but ties your code to a class name |
Never do this. Add a public getter method to the class instead |
Using __name when you mean "internal for subclasses too" |
Subclasses can't easily override or read it — name mangling isolates them | Use _name (single underscore) — the convention that respects subclasses |
| Treating private as security | Anyone can still read _ClassName__x — private is a signal, not a lock |
Don't store secrets in a Python attribute expecting cryptographic hiding |
Naming things __init or __str |
Confusing — looks like a dunder to readers; still mangled | Use _init_helper or __init_helper — clearly not a protocol name |
obj.__attr = value from outside |
Creates a per-instance attribute called literally __attr (no mangling outside the class) |
Never write to double-underscore names from outside; they're not your API |
| Overusing double underscore everywhere | Subclasses can't participate; mangled names clutter __dict__ |
Default to _x; reserve __x for genuinely private state |
| Marking everything private "just in case" | You lose the ability to compose or extend the class; testing gets painful | Start with a small public API. Make things private only when you have a reason |
Quick Reference
| Prefix | Called | What Python Does | When to Use |
|---|---|---|---|
x |
public | Nothing — fully accessible | The class's official API |
_x |
protected (convention) | Nothing — pure signal to humans | Internal, but subclasses may use |
__x |
private (name-mangled) | Renames to _ClassName__x |
Truly internal — no one else touches |
__x__ |
dunder (magic) | Nothing — reserved for protocol methods | Only when implementing Python protocols |
_x_ |
unusual — avoid | Nothing special | No convention — don't invent one |
| Task | Syntax | Notes |
|---|---|---|
| Declare private attribute | self.__balance = 1000 | Only inside the class |
| Declare private method | def __verify(self, pin): | Called via self.__verify(...) internally |
| Inspect the mangled name | obj.__dict__ | Shows _ClassName__x keys |
| Access a private (don't do it) | obj._ClassName__x | Cheating — reject in code review |
| Provide a public getter | def get_x(self): return self.__x | The right way to expose a private |
| Modern getter/setter | @property decorator | Preferred over explicit get/set methods |
Golden Rules
private keyword. It has naming
conventions and name mangling. Both are signals, not security. Anyone
determined enough can still reach in.
_x is a polite convention — "internal, don't
touch unless you know what you're doing." Python does not enforce it. Every Python
developer respects it.
__x triggers name mangling: Python
renames it to _ClassName__x. Accidental access from outside fails with
AttributeError. Deliberate access still works, but shouts "I am cheating."
__init__,
__str__) are not private. They're Python's protocol/dunder names —
reserved for special behaviour. Never invent your own __myname__.
_x. Reach for __x only when you have a specific reason
— usually sensitive state (a PIN, a token) or something that must not clash with a
subclass's attribute of the same name.
@property. Never expose the
mangled name as an "escape hatch."
__verify), formatters (__hash), internal setup
(__log_txn). Keep them small and focused; they're the ingredients of your
public methods.
__password.
__x). If subclasses might legitimately touch it → protected
(_x). If callers depend on it → public. Match the naming to your promise.