30 · Encapsulation: properties and protected names
@property turns a method into an attribute — perfect for computed values and validated writes. Python trusts you with convention over enforcement.
Python has **no enforced access control** — `_single_underscore` is a convention meaning "internal, please don't touch", and `__double_underscore` triggers name-mangling to avoid subclass collisions. `@property` lets you expose a method as a read-only (or validated-write) attribute — callers use `obj.value` syntax while the class controls what actually happens.
Without this:
Without `@property`, computed values (Fahrenheit from Celsius) require an explicit method call — `temp.to_fahrenheit()`. With `@property`, the API reads `temp.fahrenheit` — more natural and compatible with code that expects plain attributes.
Java developers often reach for private fields and getter/setter methods as a reflex. Python's philosophy is different: start with a plain public attribute. If, later, you need validation or a computed value, you can replace the attribute with a @property without changing the caller's code — because obj.x and obj.x = v look exactly the same whether x is a plain attribute or a property.
@property defines a getter that is called when you read obj.attr. @attr.setter defines a setter called when you write obj.attr = value. @attr.deleter is rare — called when you del obj.attr.
Name conventions:
_name— single leading underscore. Signals "internal, treat as private by convention". Nothing prevents access from outside — Python simply trusts you.__name— double leading underscore. Python rewrites this to_ClassName__nameat parse time (name mangling). This prevents a subclass accidentally shadowing an attribute with the same name — it is not truly private.
The Pythonic rule: don't write Java in Python. Most attributes should just be public. Add @property only when you genuinely need validation, a computed value, or lazy initialisation.
Python (in browser)
From the caller's perspective, `t.fahrenheit` looks exactly like a plain attribute — no `()` needed, assignment syntax works. The class transparently handles the conversion. If you later add the setter to a class that previously had only a getter, existing callers need no code changes.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Python does not prevent `t._celsius` access — the single underscore is a social contract, not a lock. The class author signals "this is internal plumbing" and trusts callers to respect it. In practice, 95 % of the time you should read and write through the public interface (`t.fahrenheit`, `t.celsius`).
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Name mangling's primary purpose is **collision avoidance in subclasses**, not true privacy. `_Foo__secret` is still accessible with full knowledge of the mangling scheme. Use `__double_underscore` for attributes that must not be accidentally shadowed in subclasses — not as a security measure.
In plain English, what does `@property` do?
- Python has no enforced access control. `_protected` is a convention; `__mangled` prevents subclass name collisions via name mangling (not true privacy).
- `@property` exposes a getter as `obj.attr`. `@attr.setter` adds a validated write. Callers see plain attribute syntax either way — the API surface is unchanged.
- Start with plain public attributes. Only add `@property` when you need validation, a computed value, or lazy loading.
Sklearn estimators expose hyperparameters as plain public attributes (`model.C`, `model.n_estimators`) but use `@property` for computed stats set after `.fit()` — `n_features_in_`, `classes_`, `feature_importances_`. PyTorch `nn.Module` exposes `.training` as a property that controls batch-norm and dropout behaviour; calling `.train()` or `.eval()` flips it. Pydantic models use descriptor magic similar to `@property` for field validation — the same "attribute access syntax hides computation" principle.
If you remove it: Without `@property`, every computed attribute would need to be a method call — `model.get_feature_importances()` instead of `model.feature_importances_`. The sklearn convention of attribute access for fitted parameters would be impossible to implement cleanly.