Encapsulation
Learn how to bundle data with methods and control who can touch what inside your classes.
Imagine you own a vending machine. It has money inside, a stock of snacks, and a bunch of mechanical parts. You don't want customers reaching in and grabbing cash or swapping parts around — you only want them interacting through the buttons on the front.
That idea — bundling data together with the actions that use it, and controlling what the outside world can touch — is called encapsulation. It is one of the core ideas in object-oriented programming, and Python has a clean way to express it.
See it in action
— step through the idea, then dive into the details below.Your Data, Your Rules
Encapsulation means bundling data and the methods that use it into one unit — and deciding what the outside world is allowed to touch. Think of a vending machine: customers press buttons, they don't reach inside.
#Bundling Data and Methods Together
A class already does the bundling for you. When you put attributes and methods inside a class, they travel together as one unit. That is the first half of encapsulation.
Here is a simple BankAccount class that keeps a balance and exposes only safe operations:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def get_balance(self):
return self.balance
account = BankAccount("Alice", 100)
account.deposit(50)
print(account.get_balance())Right now balance is fully public — anyone can read or change it directly. That may be fine for simple scripts, but as code grows you usually want tighter control. That is the second half of encapsulation: access control.
#Public, Protected, and Private: Python's Conventions
Python does not have a private keyword like some other languages. Instead, it uses naming conventions that are universally respected by Python programmers:
- No underscore (
name) — public. Anyone can use it. - Single underscore (
_name) — protected. It says: "this is internal; use it only if you really know what you are doing." - Double underscore (
__name) — private. Python applies name mangling to make accidental access harder.
Think of it like a house
- Public attributes are your front porch — everyone is welcome.
- Protected attributes are your living room — family and close friends, not strangers.
- Private attributes are your safe — you decide who gets the combination.
class Employee:
def __init__(self, name, salary):
self.name = name # public
self._department = "Engineering" # protected
self.__salary = salary # private (name-mangled)
emp = Employee("Bob", 80000)
print(emp.name) # fine
print(emp._department) # works but signals "be careful"
# print(emp.__salary) # AttributeError!#Name Mangling Up Close
When Python sees __salary inside a class named Employee, it quietly renames it to _Employee__salary. This is called name mangling. It stops subclasses from accidentally overwriting the same attribute, and it makes naive outside access fail with an AttributeError.
You can still get to it if you know the mangled name — Python trusts you not to abuse that.
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary
emp = Employee("Bob", 80000)
# The mangled name still works if you need it:
print(emp._Employee__salary)Python trusts you
Python's philosophy is "we are all consenting adults here." There is no hard wall. The underscore conventions are a social contract — they say "please don't touch this", not "you cannot touch this." Respect them and expect others to do the same.
#Protecting Invariants With Methods
The real reason to hide data is to protect invariants — rules that must always be true. For example, a bank balance should never go negative. If balance is public, anyone can write account.balance = -999. If you hide it and only allow changes through withdraw(), you can enforce the rule in one place.
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 > self.__balance:
raise ValueError("Not enough funds")
self.__balance -= amount
def get_balance(self):
return self.__balance
acc = BankAccount("Alice", 200)
acc.deposit(50)
acc.withdraw(30)
print(acc.get_balance())#The @property Decorator
Calling get_balance() works, but Python has a cleaner way: the `@property` decorator. It lets you expose a private attribute as if it were a plain attribute, while still running code when someone reads or writes it.
You write one method for getting (reading) and optionally one for setting (writing).
class Temperature:
def __init__(self, celsius):
self.__celsius = celsius
@property
def celsius(self):
return self.__celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self.__celsius = value
@property
def fahrenheit(self):
return self.__celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius) # calls the getter
print(t.fahrenheit) # computed on the fly
t.celsius = 100 # calls the setter
print(t.fahrenheit)Notice that fahrenheit has no setter — it is read-only. Trying to assign to it would raise an AttributeError. That is intentional: Fahrenheit is computed from Celsius, so letting someone set it directly would be confusing.
Start public, go private when needed
You do not need to hide everything from day one. Start with public attributes. When you find yourself needing to validate or compute a value, that is your signal to convert it to a @property. Python makes this refactor easy because callers never see the difference — obj.celsius looks the same whether it is a plain attribute or a property.
Forgetting the setter means read-only
If you define a @property getter but forget to add a @name.setter, the attribute is read-only. Trying to assign to it raises AttributeError: can't set attribute. This is often what you want — but when it catches you by surprise it can be confusing. Always check whether you need a setter.
#Putting It All Together
class User:
def __init__(self, username, age):
self.username = username # public
self._role = "member" # protected convention
self.__age = age # private, validated via property
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be a non-negative integer")
self.__age = value
def profile(self):
return f"{self.username} ({self.__age}) — {self._role}"
u = User("alice99", 28)
print(u.profile())
u.age = 29
print(u.age)What happens when you try to access `obj.__secret` from outside the class where `__secret` was defined?
Key takeaways
- Encapsulation means bundling data with the methods that operate on it, and controlling access to keep internals safe.
- Use no underscore for public, `_single` for protected (internal by convention), and `__double` for private (name-mangled).
- Python has no true private — the underscore system is a respected social contract, not a hard lock.
- Use `@property` to expose computed or validated attributes with clean attribute-style syntax.
- Hiding internals protects invariants: rules (like 'balance >= 0') that must always hold are enforced in one place.
What does this code print?
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance
def deposit(self, amount):
self.__balance += amount
def get_balance(self):
return self.__balance
acc = BankAccount("Alice", 100)
acc.deposit(50)
print(acc.get_balance())This code tries to print the temperature in Fahrenheit, but it crashes. What is wrong?
class Temperature:
def __init__(self, celsius):
self.__celsius = celsius
@property
def fahrenheit(self):
return self.__celsius * 9 / 5 + 32
t = Temperature(100)
t.fahrenheit = 212
print(t.fahrenheit)Complete the class so that reading u.age calls the getter and returns the private __age value.
class User: def __init__(self, age): self.__age = age @ def age(self): return self.__age u = User(25) print(u.age)
Put these lines in the right order to define a Circle class with a private radius and a read-only area property, then print the area.
def area(self):
@property
def __init__(self, radius):
self.__radius = radius
print(c.area)
return 3.14 * self.__radius ** 2
c = Circle(5)
class Circle:
Create a Rectangle class that stores width and height as private attributes. Use @property with setters to expose them, validating that each value is a positive number. Add a read-only area property that returns width * height. Test it by creating a rectangle, printing its area, updating one dimension, and printing the area again.
Try it live — edit the code and hit Run to execute real Python: