Lesson 18: Encapsulation, Properties & Magic Methods
Private attributes, @property for smart access, and dunder methods that customize objects.
Encapsulation — Protecting Data
Encapsulation keeps an object's internal state safe by controlling how it is read and changed. Python uses conventions and tools rather than hard enforcement:
| Syntax | Meaning |
|---|---|
self.name |
Public — anyone can access |
self._name |
"Protected" by convention — don't touch from outside |
self.__name |
Name-mangled — harder to accidentally access |
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # protected by convention
Mental model:
_is a "staff only" sign. Nothing in Python stops you from entering — but the convention says "you probably shouldn't".__(double underscore) actually renames the attribute internally, making accidental access fail loudly.
What You'll Learn in This Lesson
- Protect internal state with private conventions
- Build smart attribute access with
@property - Customize objects with magic (dunder) methods
Name Mangling — How __ Works
self.__balance is renamed by Python to self._ClassName__balance at runtime:
class Account:
def __init__(self):
self.__secret = 42
acc = Account()
# print(acc.__secret) # AttributeError! renamed internally
print(acc._Account__secret) # 42 — the mangled name
The double underscore exists mainly to prevent accidental clashes in inheritance — a child class can't accidentally overwrite the parent's __secret. It is not true security.
@property — Smart Attribute Access
The @property decorator turns a method into an attribute. account.balance reads like plain data but runs code:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self): # read like an attribute
return self._balance
@balance.setter
def balance(self, amount): # validate on write
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
acc = BankAccount("Amol", 1000)
print(acc.balance) # 1000 — no parentheses!
acc.balance = 1500 # setter validates
Why bother? You can add validation now without breaking existing code that reads acc.balance as a plain attribute.
Mental model:
@propertyis a guarded door. Reading is safe; writing goes through a check (the setter).
The getter-only property (read-only): omit the @setter and the attribute becomes read-only — assignment raises AttributeError. Perfect for computed values:
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self): # computed, read-only
return self.w * self.h
r = Rectangle(3, 4)
print(r.area) # 12
# r.area = 99 # AttributeError: can't set attribute
Magic (Dunder) Methods — Customize Your Objects
Double-underscore methods ("dunder" = double underscore) hook your objects into Python's operators and built-ins:
| Method | Powers | Example |
|---|---|---|
__str__ |
Friendly text for print() |
print(acc) |
__repr__ |
Exact representation for developers | repr(acc) |
__eq__ |
== between objects |
acc == other |
__lt__ |
Sorting / < |
sorted(objects) |
__len__ |
len(obj) |
len(collection) |
__getitem__ |
obj[key] |
obj[0] |
class BankAccount:
...
def __str__(self):
return f"{self.owner}'s account: Rs.{self._balance}"
def __eq__(self, other):
return self._balance == other._balance
print(acc) # Amol's account: Rs.1000 (uses __str__)
print(acc == other) # uses __eq__
str vs repr:
str()is for people;repr()is for developers. Implement both;__str__falls back to__repr__if missing.
More dunder methods worth knowing:
| Method | Powers | Typical use |
|---|---|---|
__add__ |
a + b |
Vector math, money |
__len__ |
len(obj) |
Collections |
__getitem__ |
obj[i] |
Indexable objects |
__iter__ |
for x in obj |
Iterable objects |
__call__ |
obj() |
Callable objects |
__enter__/__exit__ |
with obj: |
Context managers (Lesson 20) |
Implementing __eq__ and __lt__ together gives you ==, sorting, and min/max for free — professional objects behave exactly like built-ins.
Common Mistakes to Avoid
- Mistake: Forgetting
selfin property methods — Fix: properties are methods;selffirst. - Mistake: Naming a property and its backing attribute the same (
self.balance+@property balance) — Fix: back it withself._balance. - Mistake: Implementing only
__eq__without__hash__and putting objects in sets — Fix: if you define__eq__, set__hash__ = Noneor define__hash__explicitly. - Mistake: Expecting
__attributes to be truly private — Fix: it's name mangling, not security; use_for the convention. - Mistake: Returning a value from a setter — Fix: setters must return
None.
Professional Tips & Tricks
- Use
@propertyto add validation without breaking existing code that reads attributes. - Always implement
__str__for user-facing classes — debugging and printing become readable. - Implement
__eq__and__lt__together so your objects work with==, sorting, andmin/max. - Use getter-only properties for computed values that must stay consistent.
- Prefer
_conventions over__for most "private" data — simpler and idiomatic.
Key Takeaways
- Encapsulation controls access to internal state:
_nameby convention,__namevia name mangling. @property+@settergive attribute-like access with validation.- Dunder methods hook objects into Python operators and built-ins.
- Implement
__str__,__repr__,__eq__, and__lt__for professional objects. - Getter-only properties make clean, computed, read-only attributes.
Next up: Dataclasses — modern, boilerplate-free OOP.
# Encapsulation + property + magic methods
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # protected by convention
@property
def balance(self):
"""Read-only view of the balance."""
return self._balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def __str__(self):
return f"{self.owner}'s account: Rs.{self._balance}"
def __eq__(self, other):
return self._balance == other._balance
acc = BankAccount("Amol", 1000)
acc.deposit(500)
print(acc) # uses __str__
print("Balance via property:", acc.balance)
acc2 = BankAccount("Riya", 1500)
print("Accounts equal:", acc == acc2)Lesson Code (Python)
# Encapsulation + property + magic methods
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # protected by convention
@property
def balance(self):
"""Read-only view of the balance."""
return self._balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def __str__(self):
return f"{self.owner}'s account: Rs.{self._balance}"
def __eq__(self, other):
return self._balance == other._balance
acc = BankAccount("Amol", 1000)
acc.deposit(500)
print(acc) # uses __str__
print("Balance via property:", acc.balance)
acc2 = BankAccount("Riya", 1500)
print("Accounts equal:", acc == acc2)Console Output
Amol's account: Rs.1500
Balance via property: 1500
Accounts equal: TrueCode Visualization Tips
- Visualize _private as a 'staff only' room — the convention says do not enter.
- Picture @property as a guarded door: reading is safe, writing goes through a check (the setter).
- For __str__ vs __repr__, remember: str() is for people, repr() is for developers.
Professional Tips & Tricks
- Use @property to add validation without breaking existing code that reads attributes.
- Always implement __str__ for user-facing classes — debugging and printing become readable.
- Implement __eq__ and __lt__ together so your objects work with ==, sorting, and min/max.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: 2D Vector Magic Methods 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 18: Encapsulation, Properties, `__str__`, `__repr__` & Dunder Methods
Up next · Continue learning
Dataclasses & Modern OOP
Write less boilerplate with @dataclass, frozen data, and professional OOP best practices.