Topic 8.4
Properties and Encapsulation
In one line
Encapsulation means keeping an object's inner data behind a controlled front door. Python uses naming conventions (_name, __name) instead of locks, and @property lets you turn an attribute into a checked or computed value without changing how callers use it.
Think of it like this
A bank counter. You do not walk into the vault and take cash. You ask the teller, who checks your account and records the change. The vault is hidden; the counter is the public interface. Encapsulation means putting a counter in front of an object's data so it can enforce rules.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Encapsulation
- Keeping an object's inner data behind a controlled interface so the object can enforce its own rules.
- Public interface
- The attributes and methods other code is meant to use.
- `_name`
- A single leading underscore: a convention meaning internal, do not use from outside.
- Name mangling
- Python renames
__nameto_ClassName__nameinside a class so subclasses do not clash with it. - Property
- A method that is accessed like an attribute, created with
@property. - Getter / setter
- The code that runs when a property is read / assigned.
- Read-only attribute
- A property with no setter. Assigning to it raises
AttributeError. - Computed attribute
- A property whose value is calculated each time from other data.
- Validation
- Checking that a value is allowed before storing it.
Step by step
01The problem: nothing guards a plain attribute
With a plain attribute, acct.balance = -1000000 just works, even if a negative balance makes no sense. Nothing checks it. Every place in the program that sets the attribute would have to remember the rule.
Encapsulation moves the rule into the object so it is written once.
02One underscore: a polite sign
self._balance means "internal". Tools and humans respect it: from module import * (Topic 6.1) skips such names, and code reviewers raise an eyebrow at acct._balance from outside. It still works, as the example shows.
03Two underscores: name mangling
Inside class Account, the name __pin is stored as _Account__pin. You can see it in vars(acct). Outside the class, acct.__pin is not mangled and so finds nothing.
Its purpose is to avoid accidental clashes in inheritance, not to hide secrets. Most code never needs it. Prefer the single underscore.
04@property makes a method look like an attribute
@property def balance(self): return self._balance lets callers write acct.balance. With no setter, acct.balance = 500 raises AttributeError, so the balance can only change through methods you provide, such as deposit.
A property can also be computed: summary builds its value from other attributes every time it is read, and cannot go stale.
@property
def balance(self):
return self._balance05A setter that validates
Add @celsius.setter to a second method with the same name. It runs on every assignment, so it is the one place to enforce "not below absolute zero".
Because __init__ assigns self.celsius = celsius, the first value is checked too. If you wrote self._celsius = celsius there, a bad first value would slip through.
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("below absolute zero: " + str(value))
self._celsius = value06Two views of one value
fahrenheit has both a getter and a setter, but stores nothing of its own. It converts to and from _celsius. Callers can read or assign either scale and the two never disagree, because there is only one stored number.
07The infinite recursion mistake
If a setter assigns to its own public name (self.size = value inside size's setter), it calls itself forever and raises RecursionError (a function that keeps calling itself, Topic 5.8). The setter must write to the underscore attribute instead.
Try it yourself
- 1
Add a deposit method
In the first example add
def deposit(self, amount)that raisesValueErrorforamount <= 0and otherwise adds toself._balance. Deposit 50 and printacct.balance. The balance now changes only through a method that enforces a rule. - 2
Add a Kelvin view
In the second example add a read-only property
kelvinthat returnsself._celsius + 273.15. Printt.kelvinfor 25 degrees Celsius. - 3
Break the rule on purpose
Try
t._celsius = -500directly. No error: the underscore is a convention and the setter is bypassed. That is why you document_nameas internal and trust your teammates.
Code & diagrams
Try assigning acct.balance = 500 yourself and read the message. Python never stops you from reading acct._balance.
Expected output
100
Asha: 100
['_Account__pin', '_balance', 'owner']
AttributeError: property 'balance' of 'Account' object has no setter
100
0000
AttributeError: 'Account' object has no attribute '__pin'Change the limit or add a Kelvin property that is computed from celsius.
Expected output
25 77.0
100.0 212.0
ValueError: below absolute zero: -300
still 100.0
ValueError: below absolute zero: -500
propertyShortened. The line repeats until Python hits its recursion limit. Not run on this page because a deep recursion can crash the in-browser runner.
class Box:
def __init__(self, size):
self.size = size
@property
def size(self):
return self._size
@size.setter
def size(self, value):
self.size = value # bug: calls this same setter again
Box(3)Output
Traceback (most recent call last):
...
File "main.py", line 11, in size
self.size = value
[Previous line repeated 995 more times]
RecursionError: maximum recursion depth exceededBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Assign to a read-only property
Write acct.balance = 500 when balance has no setter.
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
acct = Account(100)
acct.balance = 500Break #2
Let the setter call itself
Write self.size = value inside the setter of size.
Myth vs fact
Myth
A leading underscore makes an attribute private and unreachable.
Fact
It is only a convention. Anyone can read and change _balance. The underscore tells other programmers not to.
Myth
Name mangling (__x) is how you hide secrets in Python.
Fact
It is a rename to _Class__x, designed to avoid name clashes in subclasses. It is not security: never store a real secret on the assumption that it is hidden.
Myth
Python code should have get_x() and set_x() methods for every attribute.
Fact
That is a Java habit. In Python, use a plain attribute first and turn it into a property later if you need rules. Callers do not change.
Myth
Properties are slow, so avoid them.
Fact
A property call costs a little more than reading a plain attribute, but the difference rarely matters. Do not put expensive work in a property that looks like an attribute, because callers will not expect it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
propertyis a data descriptor: an object with__get__and__set__(Topic 13.5 explains descriptors). Python gives data descriptors priority over the instance__dict__, which is why an instance cannot hide a property the way it can hide a plain class attribute (Topic 8.2). - ▸
Do not put slow work or side effects in a property getter. People read
obj.xexpecting a cheap lookup. If it hits the network or a database, use a method name such asfetch_x(); if the result should be calculated once, usefunctools.cached_property(Topic 9.5). - ▸
Libraries build on the same idea. Pydantic v2's
@computed_fieldwraps a property so a computed value appears in the model's serialised output. Its validation, however, uses@field_validatoron construction, not property setters. Know which one you are looking at. - ▸
Name mangling details: it happens at compile time, only to identifiers written inside the class body, and uses the class name with leading underscores stripped (
class _Cachegives_Cache__x). Sogetattr(obj, "__x")and code outside the class are never mangled. Dunder names that also end in two underscores are not mangled. - ▸
A subclass that overrides only the getter of an inherited property replaces the whole property and silently loses the setter. Reuse the parent's property with
@Parent.celsius.getter(or.setter), which copies it with one part swapped. - ▸
For read-only data, a frozen dataclass (Topic 8.8) or a
NamedTupleexpresses the intent more directly than a class full of properties.
Remember this
- 1
Python does not have real "private" fields. It relies on convention: a name starting with one underscore,
_balance, tells other programmers "this is internal, please do not touch it". Nothing stops you from touching it. Python trusts adults. - 2
A name that starts with two underscores and does not end with two,
__pin, goes through name mangling: Python renames it to_ClassName__pininside the class. This is not security. It exists to stop subclasses from accidentally reusing the same internal name.acct._Account__pinstill works. - 3
A property turns a method into something that looks like an attribute. Put
@property(a decorator, as in Topic 8.3) above a method with onlyself, andacct.balance(no brackets) calls it. Callers see a plain attribute, and you keep the freedom to compute it or check it later. - 4
A property with a setter can validate assignments. Define a second method with the same name and
@name.setter. Thent.celsius = -300runs your code, which can raise aValueError. A property without a setter is read-only: assigning to it raisesAttributeError. - 5
The usual pattern stores the real value in an underscore attribute (
self._celsius) and exposes the property under the clean name (celsius). In__init__, assign to the public name (self.celsius = celsius) so the setter's validation runs for the first value too. - 6
Properties let you start simple and grow safely. Begin with a plain attribute
self.size. If you later need validation or a computed value, change it to a property with the same name, and no calling code has to change. This is why Python programmers do not write getter and setter methods for everything, as Java programmers do.
Explain it without notes
What does encapsulation mean, and how does Python do it without private fields?
What does a leading underscore mean, and how is it different from a double underscore?
What is a property and when would you use one?
Why assign to self.celsius rather than self._celsius in __init__ when celsius has a validating setter?
Practice
Write a class Circle with a radius property that rejects negative values with ValueError, and a read-only computed property area (use 3.14 for pi).
Write a class Person with a read-only property full_name built from first and last. Show that assigning to full_name fails.
Write a class Wallet that keeps _coins internal, exposes a read-only coins property, and has spend(n) that refuses to go below zero by raising ValueError.
Trade-offs
- ↔
Properties let you add rules without breaking callers, but they hide work behind attribute syntax. Keep getters cheap and predictable; use a method name for anything slow.
- ↔
Convention-based privacy is flexible, and it relies on discipline. You trade enforced safety for simplicity and testability. Where rules really matter (money, permissions), enforce them in methods and tests, not in underscores.
Done when you can
Done when you can explain what encapsulation means and how Python's conventions support it.
Done when you can say what
_xand__xdo and why neither is security.Done when you can write a property with a validating setter and a read-only property.
Done when you can explain why
__init__should assign through the public property name.Done when you can spot and fix a setter that recurses forever.