Python Encapsulation · Examples
Object-Oriented Python

Python Encapsulation Examples

Short, runnable programs that show python encapsulation: public, protected and private in action. Read the explanation, press Run, then change the code and try your own ideas. New to the topic? Start with the Python Encapsulation: Public, Protected and Private lesson.

1One setter-style method guards every change

__init__ reuses set_target, so the temperature rule is written once and covers both creating the object and changing it later. The limits are class constants.

One setter-style method guards every change
class Thermostat:
    MIN_TEMP = 10
    MAX_TEMP = 30

    def __init__(self, target=20):
        self.set_target(target)

    def set_target(self, value):
        if not self.MIN_TEMP <= value <= self.MAX_TEMP:
            raise ValueError(f"target must be {self.MIN_TEMP}-{self.MAX_TEMP}, got {value}")
        self._target = value

    def get_target(self):
        return self._target

home = Thermostat()
home.set_target(23)
print(home.get_target())
for bad in (5, 45):
    try:
        home.set_target(bad)
    except ValueError as err:
        print("Refused:", err)
print(home.get_target())
Expected output23
Refused: target must be 10-30, got 5
Refused: target must be 10-30, got 45
23

2Name mangling in action

Inside the class, self.__combo works normally. From outside, the written name does not exist, but the mangled name _Locker__combo does. dir() shows what Python really stored.

Name mangling in action
class Locker:
    def __init__(self, combo):
        self.__combo = combo
        self.opened = False

    def try_open(self, guess):
        self.opened = guess == self.__combo
        return self.opened

locker = Locker("12-34-56")
print(locker.try_open("00-00-00"))
print(locker.try_open("12-34-56"))
print([name for name in dir(locker) if "combo" in name])
print(hasattr(locker, "__combo"), hasattr(locker, "_Locker__combo"))
Expected outputFalse
True
['_Locker__combo']
False True

3Internal helper methods use an underscore too

The underscore convention applies to methods as well. _with_fee is a detail of how spend works, so callers are not expected to use it.

Internal helper methods use an underscore too
class Wallet:
    FEE = 1

    def __init__(self, cash):
        self._cash = cash

    def _with_fee(self, amount):
        return amount + self.FEE

    def spend(self, amount):
        total = self._with_fee(amount)
        if total > self._cash:
            return False
        self._cash -= total
        return True

    def get_cash(self):
        return self._cash

wallet = Wallet(25)
print(wallet.spend(10), wallet.get_cash())
print(wallet.spend(10), wallet.get_cash())
print(wallet.spend(10), wallet.get_cash())
Expected outputTrue 14
True 3
False 3

4Hide how the data is stored

Outside code only uses add, remove and count. The dictionary inside could become a list or a database later, and no caller would need to change.

Hide how the data is stored
class Inventory:
    def __init__(self):
        self._items = {}

    def add(self, name, qty=1):
        self._items[name] = self.count(name) + qty

    def remove(self, name, qty=1):
        have = self.count(name)
        if qty > have:
            raise ValueError(f"cannot remove {qty} {name}: only {have} in stock")
        self._items[name] = have - qty

    def count(self, name):
        return self._items.get(name, 0)

shop = Inventory()
shop.add("apple", 5)
shop.remove("apple", 2)
print(shop.count("apple"), shop.count("pear"))
try:
    shop.remove("apple", 10)
except ValueError as err:
    print(err)
Expected output3 0
cannot remove 10 apple: only 3 in stock

5Reaching past the interface breaks the promise

The method refuses an impossible withdrawal. Writing to _balance directly is allowed by Python, but it skips that protection and leaves the object invalid.

Reaching past the interface breaks the promise
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)
try:
    acct.withdraw(500)
except ValueError as err:
    print("Through the method:", err)

acct._balance = -500
print("Bypassing the method:", acct.get_balance())
Expected outputThrough the method: not enough money
Bypassing the method: -500

6Return a copy of internal data

leaky_songs() returns the real list, so an outsider can add an ad to the playlist. songs() returns a copy, so the same trick changes only the copy.

Return a copy of internal data
class Playlist:
    def __init__(self):
        self._songs = []

    def add(self, title):
        self._songs.append(title)

    def leaky_songs(self):
        return self._songs

    def songs(self):
        return list(self._songs)

mix = Playlist()
mix.add("Intro")
mix.leaky_songs().append("Ad break")
mix.songs().append("Another ad")
print(mix.songs())
Expected output['Intro', 'Ad break']

Practise next

Python Encapsulation: Public, Protected and Private exercises → Back to the lesson

Python Exceptions: try, except, else and finally examplesPython Class Attributes vs Instance Attributes examplesPython @property: Getters and Setters examples