Skip to content
Laviyx Learn

Try

Python course menu

Python Encapsulation: Private Attributes, Properties and Setters

Learn encapsulation in Python: protect an object's internal data, the _single and __double underscore conventions, name mangling, read-only properties with @property, validated setters, and computed attributes.

Intermediate9 min readUpdated
On this page

Encapsulation means keeping an object's internal data behind a controlled interface. Instead of letting any code change a bank balance directly, the account offers deposit() and withdraw() methods that enforce the rules. The data and the rules that protect it live together.

The problem#

With plain attributes, nothing stops invalid changes:

Python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance

acc = BankAccount(100)
acc._balance = -999
print(acc._balance)
Output
-999

A negative balance should be impossible. Encapsulation is about making the right way easy and the wrong way obviously wrong.

Python's convention: one underscore#

Python has no truly private attributes. Instead, a name starting with _ means "internal: please don't use this from outside the class". It is a strong convention that all Python developers respect, and editors flag misuse.

  • self.balance: public, part of the interface
  • self._balance: internal, may change without notice

Read-only access with @property#

A property looks like an attribute from the outside but runs a method. Exposing _balance through a property makes it readable but not directly writable:

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

    @property
    def balance(self):
        return self._balance

acc = BankAccount("Asha", 1000)
acc.deposit(500)
print(acc.balance)               # no parentheses: looks like an attribute

try:
    acc.balance = 1_000_000
except AttributeError as e:
    print("AttributeError:", e)

try:
    acc.deposit(-50)
except ValueError as e:
    print(e)
Output
1500
AttributeError: property 'balance' of 'BankAccount' object has no setter
Deposit must be positive

The only way to change the balance is through methods that check the rules.

Validated setters#

Add a setter to allow assignment, but only of valid values:

Python
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price          # goes through the setter below

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("price cannot be negative")
        self._price = value

p = Product("Pen", 20)
p.price = 25
print(p.price)

try:
    p.price = -1
except ValueError as e:
    print(e)

try:
    Product("Bad", -3)
except ValueError as e:
    print("in __init__ too:", e)
Output
25
price cannot be negative
in __init__ too: price cannot be negative

Because __init__ assigns self.price (not self._price), even the constructor goes through the validation.

A setter can also adjust values instead of rejecting them, such as clamping a thermostat to a safe range (see the practice exercise).

Computed properties#

A property can calculate its value from other attributes, so it is always up to date:

Python
class Rectangle:
    def __init__(self, w, h):
        self.w, self.h = w, h

    @property
    def area(self):
        return self.w * self.h

r = Rectangle(3, 4)
print(r.area)
r.w = 10
print(r.area)
Output
12
40

If area were stored in __init__, it would become wrong as soon as w changed.

Double underscore: name mangling#

Two leading underscores trigger name mangling: Python renames the attribute internally to include the class name.

Python
class Secret:
    def __init__(self):
        self.__pin = 1234

s = Secret()
try:
    print(s.__pin)
except AttributeError as e:
    print("AttributeError:", e)

print(s._Secret__pin)
Output
AttributeError: 'Secret' object has no attribute '__pin'
1234

It is still reachable, so it is not a security feature. Its real purpose is to stop subclasses from accidentally overwriting a parent's internal attribute. For everyday code, a single underscore is the norm.

Why encapsulation matters#

  • Valid state: objects cannot be put into impossible states.
  • Freedom to change: you can change how data is stored internally without breaking code that uses the public interface.
  • Clear contracts: the public methods and properties document exactly how the class should be used.

Common mistakes#

Practice#

Practice exercise

Write a Thermostat class with a temp property. The setter should clamp values to the range 10–30 instead of raising an error. Set it to 35, then 5, then 24, printing the temperature each time.

Show hint

In the setter: self._temp = max(10, min(30, value)). Set self.temp in __init__ so the starting value is clamped too.

Show solution
Python
class Thermostat:
    MIN, MAX = 10, 30

    def __init__(self, temp=22):
        self.temp = temp

    @property
    def temp(self):
        return self._temp

    @temp.setter
    def temp(self, value):
        self._temp = max(self.MIN, min(self.MAX, value))

t = Thermostat()
t.temp = 35
print(t.temp)
t.temp = 5
print(t.temp)
t.temp = 24
print(t.temp)
Output
30
10
24

Key takeaway#

Finished this lesson?