Q1Which is the most accurate statement about Python's private variables?
Private Variables and Encapsulation — Safe Access via getter / setter
Learn Python's private variables and encapsulation. _x convention vs __x name mangling, get_xxx / set_xxx for safe access, and the Pythonic @property / @xxx.setter style — all hands-on.
Last time we covered the first two pillars of OOP — inheritance and polymorphism. This article wraps up the third: encapsulation.
Private Variables — Python Has No Real Privacy
Java and C++ have a private keyword that blocks outside access the moment you declare it. Python has no language-enforced privacy. Instead, the number of leading underscores signals "this is for internal use" or "don't touch this directly" — a convention between programmers, not a hard rule.
Single underscore _x — convention-only privacy
Adding a single _ in front of a name tells the Python community "this attribute is for the class's internal use — don't access it directly from outside". The __init__ parameter still uses the plain name; only the self. field gets the leading _.
class UserAccount:
def __init__(self, owner_name, balance):
self._owner_name = owner_name # internal -> prefix with _
self._balance = balance
def get_info(self): # outward-facing accessor
return {"owner": self._owner_name, "balance": self._balance}
user = UserAccount("Alice", 50000)
print(user._balance) # 50000 <- works but not recommended
print(user.get_info()) # {'owner': 'Alice', 'balance': 50000} <- recommended
Double underscore __x — name mangling
Adding two leading _ makes Python rewrite the attribute name itself. If you write self.__pin = 1234 inside an Account class, the actual stored name becomes _Account__pin. This is called name mangling — obj.__pin from outside doesn't find anything, so access is effectively much harder.
obj.__pin directly fails with AttributeError because that key isn't there.class Account:
def __init__(self, owner, pin):
self._owner = owner # convention-only privacy
self.__pin = pin # name-mangled (becomes _Account__pin)
acc = Account("Alice", 1234)
print(acc._owner) # Alice <- works normally
# print(acc.__pin) # AttributeError <- not visible directly
print(acc._Account__pin) # 1234 <- mangled name reaches it
"__" is not an absolute wall either
Double underscore prevents direct obj.__pin access, which is one notch stronger. But anyone who knows the mangled name obj._Account__pin can still reach it. It's not real privacy. In real projects, single underscore _x is far more common unless there's a specific reason to use mangling.
Encapsulation — restrict access to dedicated methods
Encapsulation is the design idea of "bundle data attributes and the methods that operate on them into one class, and force outside code to come through a small set of published entry points". So how do we build those entry points?
_price unchecked. Going through a method means the setter validates the type and range in one place.The most basic style is to write get_xxx / set_xxx methods by hand. Inside the setter, do an isinstance check for the type and a range check, and raise ValueError(...) if anything's off. With that in place, no garbage value ever reaches _price.
class Product:
def __init__(self, name, price, stock):
self._name = name
self._price = price
self._stock = stock
def get_price(self):
return self._price
def set_price(self, price):
if isinstance(price, int) and price >= 0:
self._price = price
else:
raise ValueError("price must be a non-negative integer")
product = Product("T-shirt", 1500, 30)
print(product.get_price()) # 1500
product.set_price(2000)
print(product.get_price()) # 2000
# product.set_price(-100) # ValueError
@property and @xxx.setter
The get_price() / set_price(...) style is clear, but the call sites end up looking method-call-y — not the cleanest. Python's more polished idiom uses two decorators: @property and @xxx.setter.
With those, the call site stays as product.price / product.price = 2000 — plain attribute access — but underneath, getter and setter methods are called. It's a two-layer structure where the syntax stays simple but logic still runs.
@property redirects reads and @price.setter redirects writes into method calls. Validation lives inside the setter.class Product:
def __init__(self, name, price):
self._name = name
self._price = price
@property
def price(self): # getter
return self._price
@price.setter
def price(self, value): # setter — name must match the getter
if not isinstance(value, int) or value < 0:
raise ValueError("price must be a non-negative integer")
self._price = value
@property
def label(self): # computed property — derived value
return f"{self._name} ({self._price})"
product = Product("T-shirt", 1500)
print(product.price) # 1500 <- @property is invoked
product.price = 2000 # <- @price.setter is invoked
print(product.price) # 2000
print(product.label) # T-shirt (2000) <- computed property
Keep the setter's name identical to the getter's
The price in @price.setter must match the method name from the previous @property def price. Python interprets the decorator as "attach a write version to the same price object that already has a read version" — if the name drifts, they're treated as separate things.
The Three Pillars of OOP
- Data protection — split internal state from public API via
_x - Consistency — concentrate validation in setters, one place
- Implementation freedom — change internals without changing the public API
- Pythonic style — convention plus
@property, not language enforcement
- Reuse a parent's machinery
- Same method name, different behavior per type
- Funnel outside access through a small set of doors
Knowledge Check
Answer each question one by one.
Q2What is the biggest benefit of using @property and @xxx.setter?
Q3Inside class Account: you wrote self.__pin = 1234. From outside, acc.__pin raises AttributeError. What name is actually stored on the instance?