Python Encapsulation: Public, Protected and Private
1Python Encapsulation: overview
Encapsulation means keeping an object's data behind a small set of public methods that check every change, so the object always stays valid; Python marks internal names with underscores by convention instead of enforcing secrecy.
Before this lesson: Python Classes and Objects
2Python Encapsulation: diagram
3Python Encapsulation: explained step by step
Imagine a bank account object where any line of code, anywhere in the program, can write acct.balance = -5000. Nothing stops it, and the object quietly becomes invalid. Encapsulation is the habit of keeping an object's data together with the methods that work on it, and then letting the data change only through those methods, so they can check every request.
Why hide internal data?
Here is a class with no protection at all:
class NaiveAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
acct = NaiveAccount("Asha", 100)
acct.balance = -5000 # nothing stops this
print(acct.owner, acct.balance)A balance of -5000 can never be right, but the class has no way to refuse it. Keeping the data behind methods gives you three things:
- The object stays valid. The rules (a balance is never negative) live in one place, so no other code can break them by accident.
- You can change the inside later. If code outside only uses
deposit()andget_balance(), you can store the money as cents or in a list tomorrow and nobody else has to change. - It is simpler to use. Other programmers only need to learn a handful of methods, not every detail of how the object works.
Public, protected and private
Languages like Java have keywords such as private that the compiler enforces. Python has no such keywords. It uses naming conventions instead:
| Name | Usually called | What it means |
|---|---|---|
balance |
public | Part of the interface. Use it freely. |
_balance |
protected (internal) | An internal detail. Please don't touch it from outside. This is a convention only. |
__balance |
private | Python renames it to _ClassName__balance, which avoids accidental name clashes. |
One underscore: a sign on the door
A single leading underscore is a message to other programmers: this is an internal detail, use the methods instead.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # internal: use the methods
def get_balance(self):
return self._balance
acct = BankAccount("Asha", 100)
print(acct.get_balance())
print(acct._balance) # works, but you are ignoring the signPython does not block that last line. The underscore works like a "staff only" sign: it tells everyone that this name may change without warning, and that code relying on it is on its own.
Two underscores: name mangling
A name that starts with two underscores (and does not end with two) is mangled: Python rewrites it to _ClassName__name when it reads the class. Code inside the class is rewritten too, so the methods keep working. Code outside asks for a name that no longer exists.
class Vault:
def __init__(self, code):
self.__code = code
def check(self, guess):
return guess == self.__code
vault = Vault(4821)
print(vault.check(4821))
print(sorted(vars(vault)))
print([name for name in dir(vault) if "code" in name])
print(vault._Vault__code)
try:
print(vault.__code)
except AttributeError as err:
print("AttributeError:", err)The attribute is still there under its mangled name, so anyone can reach it. Mangling is not security. Its real purpose is to stop a name from clashing by accident, for example when someone later builds a new class on top of yours and picks the same attribute name. Most Python code never needs it and uses a single underscore instead.
A small public interface
Think of the public methods as the buttons on a machine: deposit, withdraw, get_balance. Everything else stays inside. Every method that changes the data checks the request first. When a value makes no sense, the method raises ValueError (the right type of value, but an unacceptable one). That is louder than quietly returning False, because a caller cannot ignore an exception by accident. If you need a reminder of how try and except work, see the exceptions lesson.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive")
if amount > self._balance:
raise ValueError("not enough money")
self._balance -= amount
def get_balance(self):
return self._balance
acct = BankAccount("Asha", 100)
acct.deposit(50)
for bad in (-20, 500):
try:
acct.withdraw(bad)
except ValueError as err:
print("Refused:", err)
print(acct.get_balance())Both bad requests are refused and the balance is still 150. The rule "never negative" is enforced in exactly one place.
Getter-style methods
get_balance() is a getter: a method whose job is to hand back an internal value. Its partner, a setter, stores a new value after checking it. In Python you only write them when they add something, such as validation. An attribute with no rules can simply be public, and a plain owner attribute is perfectly fine.
Calling acct.get_balance() is a little clumsy compared with acct.balance. The next lesson, on properties, shows how to keep the nice attribute-style spelling and still run your checks behind the scenes.
Don't hand out your internals
If a getter returns an internal list or dictionary, the caller receives the real object and can change it behind your back. Return a copy instead:
def get_songs(self):
return list(self._songs) # a copy: callers cannot change the original
"We are all consenting adults"
Python's designers trust programmers. Instead of locking things away, the language gives you conventions and expects you to follow them. Treat an underscore as a contract: use the public methods, and if you do reach into _something (for example while debugging), know that the class's author is free to break it in the next version.
Which one should I use?
- Start with a public name.
- Use
_namefor helper methods and for data that has rules to protect. - Use
__nameonly when you really need to avoid clashes between classes, which is rare. - Anything that must never be wrong (a balance, a temperature range) should be changed only by methods that check it.
4Python Encapsulation: syntax
class Account:
def __init__(self, owner, balance=0):
self.owner = owner # public: part of the interface
self._balance = balance # protected (convention): internal, please don't touch
self.__pin = "0000" # private (mangled): stored as _Account__pin
def deposit(self, amount): # public method: the safe way to change the data
if amount <= 0:
raise ValueError("amount must be positive")
self._balance += amount
def get_balance(self): # getter-style method
return self._balance
acct = Account("Asha")
acct.deposit(50) # use the public interface
acct._balance # works, but ignores the convention
acct._Account__pin # the mangled name: works, but a bad idea
acct.__pin # AttributeError: no attribute called __pin
5Python Encapsulation: run the example
Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
self.__pin = "4821"
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def get_balance(self):
return self._balance
acct = BankAccount("Asha", 100)
acct.deposit(50)
print(acct.get_balance())
try:
acct.deposit(-30)
except ValueError as err:
print("Refused:", err)
print(sorted(vars(acct)))Expected output150
Refused: Deposit must be positive
['_BankAccount__pin', '_balance', 'owner']
7Python Encapsulation: how the example works
Hover or tap a step to highlight its lines in the example above.
- Line 1-2
BankAccountstarts a class whose__init__takes an owner and an optional starting balance. - Line 3
owneris a public attribute. It needs no protection, so any code may read it. - Line 4
_balancehas one leading underscore: it is internal, and the rest of the program is expected to use methods instead of touching it. - Line 5
__pinhas two leading underscores, so Python stores it under the mangled name_BankAccount__pin. - Line 7-10
depositis part of the public interface. It checks the amount first and raisesValueErrorfor zero or negative amounts, so a bad value never reaches_balance. - Line 12-13
get_balanceis a getter: it returns the internal value so outside code never needs to read_balancedirectly. - Line 15-17The account starts with 100. A valid deposit of 50 goes through the public method, so the balance becomes 150, and
get_balance()reports it. - Line 18-21The invalid deposit of -30 raises
ValueError. Theexceptblock catches it and prints the message, and the balance stays 150. - Line 22
vars(acct)lists the real attribute names.sorted()makes the order predictable. You can see_balanceas written and the mangled_BankAccount__pin. In sorted order, the capitalBcomes before the lowercaseb.
8Python Encapsulation: common mistakes
Reading a double-underscore attribute from outside
✗ Wrong
class Vault:
def __init__(self, code):
self.__code = code
vault = Vault(4821)
print(vault.__code)✓ Right
class Vault:
def __init__(self, code):
self.__code = code
def check(self, guess):
return guess == self.__code
vault = Vault(4821)
print(vault.check(4821))Inside the class the name is mangled to _Vault__code, so outside code cannot find __code. Give the class a method that exposes what outsiders legitimately need, instead of reaching for the attribute.
Bypassing the methods and breaking the rules
✗ Wrong
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("not enough money")
self._balance -= amount
def get_balance(self):
return self._balance
acct = BankAccount(100)
acct._balance = -9999
print(acct.get_balance())✓ Right
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("not enough money")
self._balance -= amount
def get_balance(self):
return self._balance
acct = BankAccount(100)
acct.withdraw(40)
print(acct.get_balance())Python lets you write to _balance, but that skips every check in withdraw. Treat the underscore as a contract and change the data only through the public methods.
Thinking a double underscore keeps a secret
✗ Wrong
class BankAccount:
def __init__(self, pin):
self.__pin = pin
acct = BankAccount("4821")
print(acct._BankAccount__pin)✓ Right
class BankAccount:
def __init__(self, pin):
self.__pin = pin
def check_pin(self, guess):
return guess == self.__pin
acct = BankAccount("4821")
print(acct.check_pin("0000"))
print(acct.check_pin("4821"))Name mangling only renames the attribute to avoid accidents; anyone can type the new name. Never rely on it to protect a real secret. Keep passwords and PINs out of ordinary objects, or store only a hash.
Not handling the ValueError your own method raises
✗ Wrong
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("not enough money")
self._balance -= amount
acct = BankAccount(100)
acct.withdraw(500)
print("done")✓ Right
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("not enough money")
self._balance -= amount
acct = BankAccount(100)
try:
acct.withdraw(500)
except ValueError as err:
print("Refused:", err)
print("done")Raising an exception is how the method refuses a bad request, and it stops the program unless the caller handles it. Wrap the call in try and except ValueError and decide what to do when the request is refused.
Checking the rule in only one of the places data changes
✗ Wrong
class Thermostat:
def __init__(self, target):
if not 10 <= target <= 30:
raise ValueError("target must be between 10 and 30")
self._target = target
def set_target(self, value):
self._target = value
def get_target(self):
return self._target
home = Thermostat(21)
home.set_target(99)
print(home.get_target())✓ Right
class Thermostat:
def __init__(self, target):
self.set_target(target)
def set_target(self, value):
if not 10 <= value <= 30:
raise ValueError("target must be between 10 and 30")
self._target = value
def get_target(self):
return self._target
home = Thermostat(21)
try:
home.set_target(99)
except ValueError as err:
print("Refused:", err)
print(home.get_target())Every way of changing the data must enforce the rule. The easiest way to keep it in one place is to let __init__ call the checking method.
9Python Encapsulation: exercises
Write your answer, then press Check. The checks run in your browser.
1. A piggy bank that says no
Write code Easy ✓ CompletedPiggyBank keeps its savings in the internal attribute _total. Finish two methods:
add(amount)addsamountto the savings, but raisesValueErrorifamountis zero or negative.get_total()returns the savings.
class PiggyBank:
def __init__(self):
self._total = 0
def add(self, amount):
# raise ValueError if amount is 0 or negative, otherwise add it to _total
pass
def get_total(self):
# return the savings
pass
bank = PiggyBank()
bank.add(5)
bank.add(10)
print(bank.get_total())
if amount <= 0: first and raise ValueError(...) inside it. Only after the check, do self._total += amount.class PiggyBank:
def __init__(self):
self._total = 0
def add(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._total += amount
def get_total(self):
return self._total
bank = PiggyBank()
bank.add(5)
bank.add(10)
print(bank.get_total())
2. Predict what Python stored
Predict the output Medium ✓ CompletedSafe hides its code with a double underscore. Type exactly what the program prints.
class Safe:
def __init__(self, code):
self._owner = "Mia"
self.__code = code
def try_code(self, guess):
return guess == self.__code
box = Safe(1234)
print(box.try_code(1234))
print(sorted(vars(box)))
print(box._Safe__code)
print(hasattr(box, "__code"))
__code becomes _Safe__code. sorted() puts a capital S before a lowercase o. Outside the class, nothing is called __code.True
['_Safe__code', '_owner']
1234
False
2 more exercises → 6 more examples
Questions learners ask
Does Python have private variables?
Not in the strict sense. Python has no private keyword and cannot truly lock an attribute away. It uses conventions: a leading underscore (_balance) means internal, and a double leading underscore (__balance) triggers name mangling to _ClassName__balance, which helps avoid accidental clashes. Both can still be reached by determined code.
What is the difference between _x and __x in Python?
_x is only a convention: Python treats it like any other name, but programmers read it as "internal, please don't use". __x (two underscores) is changed by Python to _ClassName__x inside the class, so the written name no longer works from outside. Use the single underscore in almost every case.
Do I need getters and setters in Python?
Usually not. If an attribute has no rules, make it a plain public attribute. When you need validation, write a method that checks the value, or use a property (the next lesson) so that obj.value still looks like a normal attribute but runs your checks.
Further reading
10Next lesson
Up nextPython @property: Getters and SettersA property is a method that Python lets you use like a plain attribute, so obj.price looks like simple data access whil…Related:
Python Exceptions: try, except, else and finallyPython Class Attributes vs Instance AttributesPython @property: Getters and Setters
← Python Class Methods and Static Methods · All Object-Oriented Python lessons · Practise in the IDE