Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~26 min· topic 4 of 12

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 __name to _ClassName__name inside 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.

main.pywhole filepython
    @property
    def balance(self):
        return self._balance

05A 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.

main.pywhole filepython
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero: " + str(value))
        self._celsius = value

06Two 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.

Two views of one valuediagram
Rendering diagram…

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. 1

    Add a deposit method

    In the first example add def deposit(self, amount) that raises ValueError for amount <= 0 and otherwise adds to self._balance. Deposit 50 and print acct.balance. The balance now changes only through a method that enforces a rule.

  2. 2

    Add a Kelvin view

    In the second example add a read-only property kelvin that returns self._celsius + 273.15. Print t.kelvin for 25 degrees Celsius.

  3. 3

    Break the rule on purpose

    Try t._celsius = -500 directly. No error: the underscore is a convention and the setter is bypassed. That is why you document _name as internal and trust your teammates.

Code & diagrams

Underscores, mangling and a read-only property New tab

Try assigning acct.balance = 500 yourself and read the message. Python never stops you from reading acct._balance.

Sign in to run this example in your browser.

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'
A validated property with two views New tab

Change the limit or add a Kelvin property that is computed from celsius.

Sign in to run this example in your browser.

Expected output

25 77.0
100.0 212.0
ValueError: below absolute zero: -300
still 100.0
ValueError: below absolute zero: -500
property
The recursion trap (not runnable here)python

Shortened. 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 exceeded

Break 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.

main.pywhole filepython
class Account:
    def __init__(self, balance):
        self._balance = balance

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


acct = Account(100)
acct.balance = 500
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 12, in <module>
acct.balance = 500
^^^^^^^^^^^^
AttributeError: property 'balance' of 'Account' object has no setter

Break #2

Let the setter call itself

Write self.size = value inside the setter of size.

terminal
$ python main.py
── what you'll see ──
RecursionError: maximum recursion depth exceeded

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.

  • ▸

    property is 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.x expecting a cheap lookup. If it hits the network or a database, use a method name such as fetch_x(); if the result should be calculated once, use functools.cached_property (Topic 9.5).

  • ▸

    Libraries build on the same idea. Pydantic v2's @computed_field wraps a property so a computed value appears in the model's serialised output. Its validation, however, uses @field_validator on 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 _Cache gives _Cache__x). So getattr(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 NamedTuple expresses the intent more directly than a class full of properties.

Remember this

  1. 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. 2

    A name that starts with two underscores and does not end with two, __pin, goes through name mangling: Python renames it to _ClassName__pin inside the class. This is not security. It exists to stop subclasses from accidentally reusing the same internal name. acct._Account__pin still works.

  3. 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 only self, and acct.balance (no brackets) calls it. Callers see a plain attribute, and you keep the freedom to compute it or check it later.

  4. 4

    A property with a setter can validate assignments. Define a second method with the same name and @name.setter. Then t.celsius = -300 runs your code, which can raise a ValueError. A property without a setter is read-only: assigning to it raises AttributeError.

  5. 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. 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

01

What does encapsulation mean, and how does Python do it without private fields?

02

What does a leading underscore mean, and how is it different from a double underscore?

03

What is a property and when would you use one?

04

Why assign to self.celsius rather than self._celsius in __init__ when celsius has a validating setter?

Practice

01

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).

02

Write a class Person with a read-only property full_name built from first and last. Show that assigning to full_name fails.

03

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 _x and __x do 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.