Python Encapsulation
Object-Oriented Python Object-Oriented Python

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.

GoalBy the end you can explain why objects protect their data, use public, _protected and __private names correctly, show what name mangling really does, design a small public interface that raises ValueError for bad input, and avoid leaking internal data.

Before this lesson: Python Classes and Objects

2Python Encapsulation: diagram

Python Encapsulation: Public, Protected and PrivateOutside codeacct.deposit(50), acct.get_balance()Public methods: the safe way indeposit(), withdraw(), get_balance()Internal data: checked by the methods_balance = 150Outside code goes through the methods; only the methods touch _balance.
Think of the object in layers. Outside code talks to the public methods, and those methods look after the internal data, so a bad value is refused before it can reach _balance.

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:

Python
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() and get_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.

Python
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 sign

Python 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.

Python
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.

Python
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 _name for helper methods and for data that has rules to protect.
  • Use __name only 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.

Interactive example
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.

  1. Line 1-2BankAccount starts a class whose __init__ takes an owner and an optional starting balance.
  2. Line 3owner is a public attribute. It needs no protection, so any code may read it.
  3. Line 4_balance has one leading underscore: it is internal, and the rest of the program is expected to use methods instead of touching it.
  4. Line 5__pin has two leading underscores, so Python stores it under the mangled name _BankAccount__pin.
  5. Line 7-10deposit is part of the public interface. It checks the amount first and raises ValueError for zero or negative amounts, so a bad value never reaches _balance.
  6. Line 12-13get_balance is a getter: it returns the internal value so outside code never needs to read _balance directly.
  7. 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.
  8. Line 18-21The invalid deposit of -30 raises ValueError. The except block catches it and prints the message, and the balance stays 150.
  9. Line 22vars(acct) lists the real attribute names. sorted() makes the order predictable. You can see _balance as written and the mangled _BankAccount__pin. In sorted order, the capital B comes before the lowercase b.

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)
AttributeError: 'Vault' object has no attribute '__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())
No error, but it prints -9999: the account is now invalid

✓ 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)
No error, but it prints 4821: the "private" value was read

✓ 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")
ValueError: not enough money

✓ 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())
No error, but it prints 99: the rule was only checked in __init__

✓ 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

PiggyBank keeps its savings in the internal attribute _total. Finish two methods:

  • add(amount) adds amount to the savings, but raises ValueError if amount is zero or negative.
  • get_total() returns the savings.
Your code
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())

2. Predict what Python stored

Predict the output Medium

Safe hides its code with a double underscore. Type exactly what the program prints.

Read this code
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"))

2 more exercises → 6 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

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