Encapsulation & @property Decorators
Protect internal class state with private conventions, validated getters, and setter mutation guards.
Python Trusts You — With Conventions, Not Locks
Unlike languages with compiler-enforced private keywords, Python relies on naming conventions: a single leading underscore (_attr) signals "internal, please don't touch," but nothing stops external code from touching it anyway.
A double leading underscore (__attr) is different — it triggers real name mangling, rewriting the name to _ClassName__attr internally. This exists to prevent accidental collisions in subclasses, not to provide security.
# still reachable via _Class__password
def celsius(self, v):
if v < -273.15: raise ValueError(...)
An underscore prefix is like a "staff only" sign on an office door — it's a polite request, not a locked door. @property is like a reception desk: visitors interact with it using simple, friendly syntax, while the actual validation and computation happen behind the counter.
Inspecting Single vs Double Underscore Attributes
Run the snippet below to see exactly how double-underscore mangling changes an attribute's real name:
class SecureVault:
def __init__(self, key, secret_data):
self.key = key # Public
self._audit_tag = "V1" # Protected by convention
self.__secret = secret_data # Name-mangled
vault = SecureVault("API_KEY", "TOP_SECRET_PAYLOAD")
print("Public:", vault.key)
print("Protected:", vault._audit_tag)
# Direct access to __secret raises AttributeError:
try:
print(vault.__secret)
except AttributeError as err:
print("Direct access caught:", err)
# But the mangled attribute is readily accessible in __dict__:
print("Mangled attribute accessed:", vault._SecureVault__secret)A community signal meaning "internal implementation detail" — Python does not enforce it.
Rewritten to _ClassName__attr to avoid accidental subclass attribute collisions.
Lets attribute-style reads/writes actually run getter/setter methods underneath.
Computes an expensive read-only property once, then reuses the cached result.
Compare Every Visibility Level
Click through public, protected, mangled, and property-based attribute access:
No leading underscore means 'use this freely' — the default for anything meant to be part of the public interface.
Backing Variables, Validation, and Caching
These three patterns cover nearly every real-world use of @property.
Backing variable pattern
self._val = val # inside setterAlways store property state in a differently-named backing attribute to avoid infinite recursion.
self.val = val # inside __init__Assigning through the property in __init__ ensures constructor inputs get the same validation as later writes.
Getter / setter validation
@celsius.setter
def celsius(self, value): ...The setter runs on every assignment, letting you reject invalid values before they're stored.
Cached computation
@functools.cached_propertyMemoizes an expensive read-only computed property on immutable objects, computing it only once.
Worked Hands-on Walkthroughs
Progress step-by-step from underscore conventions through validated properties to read-only computed access:
Inspecting Single vs Double Underscore Attributes
Name mangling renames __secret to _SecureVault__secret to avoid collision, not to enforce cryptographic secrecy.
class SecureVault:
def __init__(self, key, secret_data):
self.key = key # Public
self._audit_tag = "V1" # Protected by convention
self.__secret = secret_data # Name-mangled
vault = SecureVault("API_KEY", "TOP_SECRET_PAYLOAD")
print("Public:", vault.key)
print("Protected:", vault._audit_tag)
# Direct access to __secret raises AttributeError:
try:
print(vault.__secret)
except AttributeError as err:
print("Direct access caught:", err)
# But the mangled attribute is readily accessible in __dict__:
print("Mangled attribute accessed:", vault._SecureVault__secret)Name mangling renames __secret to _SecureVault__secret to avoid collision, not to enforce cryptographic secrecy.
Comprehensive Property Lifecycle with Validation
Assigning to sensor.celsius in __init__ and later assignments both execute the setter validation.
class TemperatureSensor:
def __init__(self, celsius=0.0):
# Channel through property setter for initial validation!
self.celsius = celsius
@property
def celsius(self):
"""Read the temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Validate physical lower bound (Absolute Zero)."""
if not isinstance(value, (int, float)):
raise TypeError("Temperature must be a numerical value")
if value < -273.15:
raise ValueError("Temperature cannot drop below Absolute Zero (-273.15°C)")
self._celsius = float(value)
@celsius.deleter
def celsius(self):
raise PermissionError("Cannot delete primary sensor reading")
sensor = TemperatureSensor(25.0)
print(f"Initial temp: {sensor.celsius}°C")
sensor.celsius = 100.0
print(f"Boiling temp: {sensor.celsius}°C")
try:
sensor.celsius = -300.0
except ValueError as err:
print("Validation caught:", err)Assigning to sensor.celsius in __init__ and later assignments both execute the setter validation.
Computed Read-Only Properties vs Cached Properties
The checksum computation ran only once; the second read retrieved the cached value immediately.
import time
from functools import cached_property
class DatasetAnalyzer:
def __init__(self, data_points):
self.data_points = list(data_points)
# 1. Computed property: recalculates whenever accessed
@property
def count(self):
return len(self.data_points)
# 2. Cached property: expensive work performed once per instance
@cached_property
def checksum(self):
print("--- Computing expensive SHA256 checksum ---")
time.sleep(0.01) # Simulating heavy cryptographic work
return sum(self.data_points) % 10007
analyzer = DatasetAnalyzer([10, 20, 30, 40, 50])
print("Count:", analyzer.count)
# First access calculates
print("First checksum read:", analyzer.checksum)
# Second access reads directly from cache (__dict__)
print("Second checksum read (cached):", analyzer.checksum)The checksum computation ran only once; the second read retrieved the cached value immediately.
Common Traps & How to Avoid Them
Study the mistakes and clean fixes below before writing your own properties:
# Mistyped variable or missing closing parenthesis
result = encapsulation____property_val# Declare and verify variables before execution
val = 10
print("Defined correctly:", val)Beginners frequently encounter typos or forgotten definitions before using an identifier.
# Modifying shared state or unexpected variable shadowing
def update_state(data=[]):
data.append(1)
return datadef update_state(data=None):
if data is None:
data = []
data.append(1)
return dataDefault arguments in Python are evaluated once at function definition time, not at invocation time.
# Opening resources in a tight loop without closing or bounding memory
files = [open(f"log_{i}.txt", "w") for i in range(5000)]for i in range(5):
with open(f"log_{i}.txt", "w") as f:
f.write("Safe")Failing to release system resources leads to process termination under heavy production workloads.
Real-World Systems in Action
See how scripts, cloud microservices, and distributed systems rely on encapsulated, validated state:
Local Developer Automation Tool
Individual developers and IT administrators automate everyday repetitive workflows.
A clean standalone script providing fast, local automation with zero dependencies.
class BankAccount:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
acc = BankAccount(1000)
print("Balance:", acc.balance)Backend API Service Integration
SaaS platforms run backend microservices processing requests concurrently.
A modular service handler integrating validation and structured responses.
# Service integration layer
def handle_request(payload: dict) -> dict:
return {"status": "success", "processed": True}High-Throughput Cloud Architecture
Domain-Driven Design (DDD) prevents invalid state corruption across microservice entities using encapsulated property setters.
Domain-Driven Design (DDD) prevents invalid state corruption across microservice entities using encapsulated property setters.
# High-resilience production pattern
import time
def process_stream(events: list) -> int:
processed = 0
for event in events:
if event:
processed += 1
return processedKey Points & Concept Review
- Python relies on community visibility conventions (_attr) rather than hard compiler private access modifiers.
- Double-underscore attributes (__attr) trigger name mangling to prevent accidental subclass override collisions, not for security.
- Use @property to expose simple attribute syntax while executing dynamic computation or validation under the hood.
- Always store property state in a backing variable (e.g. self._val) to prevent infinite recursion.
- Assign to properties in __init__ (self.val = val) to ensure constructor inputs undergo identical validation checks.
- Leverage functools.cached_property to memoize expensive, read-only property computations on immutable objects.
Frequently Asked Questions & Reference Hub
Forgetting the backing variable and triggering infinite recursion in a property setter is a completely normal rite of passage — once you've debugged it once, the self._val pattern never leaves your muscle memory.
See exactly what each underscore prefix means.
Understand why double-underscore attributes get rewritten.
Learn how @property exposes validated attribute access.
See how to memoize an expensive read-only computed attribute.
Learn the classic mistake and its one-line fix.
Compare Python's approach against languages with private keywords.
You've Mastered Encapsulation & @property!
You understand visibility conventions, name mangling, and validated computed properties. Now lock in your knowledge with hands-on practice.
Write a validated @property with a proper backing variable.
Earn experience points toward your developer rank as soon as you pass.
Bite-sized knowledge check with instant explanations for every question.
Worked example
Validate attribute assignment transparently with @property.
class Box:
def __init__(self, s): self._s = s
@property
def s(self): return self._s
print(Box(10).s)
How it works, step by step
- @property defines a getter accessed like an attribute.
Encapsulation & @property Decorators practice
Create a class User with an __init__(self, username, age) that stores self.username = username and uses a @property for age. The setter should raise ValueError("Age cannot be negative") if new_age < 0, otherwise store in self._age. Create u = User("Sam", 25), update u.age = 26, and print: User Sam is 26 years old.
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Encapsulation & @property Decorators quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.