Python Tutorial
Properties and Descriptors
In Python you start with plain public attributes. If you later need validation, a computed value or a read-only field, you do not have to change every caller from obj.price to obj.get_price() — you turn the attribute into a property. Properties are built on a more general mechanism, descriptors, which also power methods, classmethod, staticmethod and ORM fields.
This lesson covers @property with setters and deleters, computed and cached properties, and writing reusable descriptors with __get__, __set__ and __set_name__.
@property, Setters and Deleters
Decorating a method with @property makes it accessible like an attribute. Add @name.setter to validate or transform assigned values and @name.deleter for deletion. A property without a setter is read-only. Store the real value in a "private" attribute such as self._price.
Computed and Cached Properties
Properties can compute values on the fly — full_name from first and last name, area from width and height — so derived data never goes stale. For expensive computations on objects that do not change, functools.cached_property computes the value once and stores it on the instance.
Descriptors
A descriptor is an object stored as a class attribute that defines __get__ (and optionally __set__, __delete__). Python calls these when the attribute is accessed on instances. __set_name__ tells the descriptor which attribute name it was assigned to. Descriptors let you write validation once — "positive number", "non-empty string" — and reuse it across many classes and fields; this is how Django and SQLAlchemy model fields work.
Examples
Validated attributes and computed properties
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("width must be positive")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("height must be positive")
self._height = value
@property
def area(self): # computed, read-only
return self.width * self.height
r = Rectangle(3, 4)
print(r.area)
r.width = 10
print(r.area)
try:
r.height = -1
except ValueError as e:
print("ValueError:", e)
try:
r.area = 5
except AttributeError as e:
print("AttributeError:", e)
12
40
ValueError: height must be positive
AttributeError: property 'area' of 'Rectangle' object has no setter
Deleters and cached_property
from functools import cached_property
class User:
def __init__(self, first, last):
self.first, self.last = first, last
self._email = None
@property
def full_name(self):
return f"{self.first} {self.last}"
@property
def email(self):
return self._email or "not set"
@email.setter
def email(self, value):
self._email = value.strip().lower()
@email.deleter
def email(self):
self._email = None
class Report:
def __init__(self, rows):
self.rows = rows
@cached_property
def total(self):
print("computing total...")
return sum(self.rows)
u = User("Asha", "Rao")
u.email = " Asha@Webnest.IN "
print(u.full_name, u.email)
del u.email
print(u.email)
rep = Report([10, 20, 30])
print(rep.total, rep.total)
Asha Rao asha@webnest.in
not set
computing total...
60 60
A reusable validating descriptor
class Positive:
def __set_name__(self, owner, name):
self.private_name = "_" + name
self.public_name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name)
def __set__(self, obj, value):
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"{self.public_name} must be a positive number, got {value!r}")
setattr(obj, self.private_name, value)
class Product:
price = Positive()
stock = Positive()
def __init__(self, name, price, stock):
self.name, self.price, self.stock = name, price, stock
p = Product("Pen", 10, 100)
print(p.price, p.stock, vars(p))
for field, value in [("price", -5), ("stock", "many")]:
try:
setattr(p, field, value)
except ValueError as e:
print("ValueError:", e)
10 100 {'name': 'Pen', '_price': 10, '_stock': 100}
ValueError: price must be a positive number, got -5
ValueError: stock must be a positive number, got 'many'
Common Mistakes
- Storing the value under the same name as the property (self.width inside the width setter), causing infinite recursion.
- Writing get_x/set_x methods everywhere instead of starting with plain attributes and switching to properties later.
- Using cached_property on objects whose inputs change, leaving a stale cached value.
- Putting slow work or side effects in properties that callers assume are cheap.
Key Points to Remember
- @property turns methods into attribute-style access; add .setter and .deleter as needed.
- Properties allow validation, read-only fields and computed values without changing callers.
- cached_property computes once per instance.
- Descriptors (__get__, __set__, __set_name__) make reusable attribute logic.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.