In languages like Java, we write getName() and setName() methods to control access to attributes. Python says: forget that noise. We have @property — it lets us use regular attribute syntax while running custom logic behind the scenes.
The Problem
Say we want to validate an age before setting it. Without properties, we’d need getter/setter methods and everyone would have to remember to call them.
# The Java way — not Pythonic
class Person:
def get_age(self):
return self._age
def set_age(self, value):
if value < 0:
raise ValueError("Age can't be negative")
self._age = value
@property to the Rescue
With @property, we access age like a normal attribute, but Python secretly calls our getter/setter.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age # this calls the setter!
@property
def age(self): # getter
return self._age
@age.setter
def age(self, value): # setter
if value < 0:
raise ValueError("Age can't be negative")
self._age = value
p = Person("Manish", 25)
print(p.age) # 25 — calls the getter
p.age = 30 # calls the setter
p.age = -5 # ValueError: Age can't be negative
Notice we store the actual value in self._age (with underscore) but expose it as self.age. The underscore is a convention meaning “private, don’t touch directly.”
Read-Only Properties
If we only define the @property getter and skip the setter, the attribute becomes read-only.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self): # computed, read-only
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.area) # 78.53975
c.area = 100 # AttributeError: can't set attribute
This is great for computed properties — values derived from other attributes.
The Deleter
Less common, but we can also define what happens when someone uses del.
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@name.deleter
def name(self):
print("Deleting name...")
self._name = None
p = Person("Manish")
del p.name # Deleting name...
Validation in Setters
Properties really shine when we need to enforce rules. We can keep all validation in one place.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # triggers setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
@property
def fahrenheit(self): # computed from celsius
return self._celsius * 9/5 + 32
In simple language, @property lets us add logic to attribute access without changing how the outside world uses our class. No ugly get_x() calls — just clean obj.x syntax with validation and computation happening under the hood.