Properties & Getters/Setters
Python lets you access an attribute using plain dot syntax like person.age, with no parentheses, even when reading that value actually runs code behind the scenes. This is done with the @property decorator. It turns an ordinary-looking method into something that behaves like an attribute from the outside, while internally it can compute a value, look something up, or validate data before returning or storing it.
Writing a Getter with @property
Decorate a method with @property and callers can read it without calling it as a function. Here, full_name is computed on the fly from first and last, but it reads just like a regular attribute.
class Employee:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def full_name(self):
return f"{self.first} {self.last}"
emp = Employee("Ada", "Lovelace")
print(emp.full_name) # no parentheses neededAda Lovelace
Adding a Setter with @x.setter
A property defined this way is read-only by default — trying to assign to it raises an AttributeError. To allow writes, define a second method with the same name, decorated with @full_name.setter (using the property's own name, not a fixed keyword). Python pairs the getter and setter together because they share the same name.
Full Example: Validation with a Setter
The most common real-world use of properties is validation. Below, Person stores its age internally in a "private" attribute named _age (the leading underscore is a Python convention meaning "internal, don't touch directly"). The public age property reads from _age in its getter, and the setter checks the value before storing it, raising ValueError for anything negative.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age # goes through the setter below
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("age cannot be negative")
self._age = value
person = Person("Grace", 30)
print(person.age) # reads normally, like an attribute
person.age = 31 # writes normally, runs validation
print(person.age)
person.age = -5 # triggers the setter's check30 31 ValueError: age cannot be negative
Notice that both reading and writing person.age use ordinary attribute syntax — there's no person.get_age() or person.set_age(31) anywhere. The validation logic is completely invisible to the calling code.
Why This Beats Explicit get_age()/set_age() Methods
Languages like Java commonly use explicit getter and setter methods (getAge(), setAge()) because plain public fields offer no place to add validation later without breaking every caller. Python's @property solves the same problem differently, and more elegantly, for two reasons:
No syntax change for callers: reading and writing still look exactly like plain attribute access —
person.age,person.age = 31— even though real code runs on every access.Backward compatibility as your class grows: you can start a class with a plain public attribute (
self.age = age), ship it, and let other code useperson.agefreely. Later, if you need validation or a computed value, you convert it to a@propertywith the same name — every existing caller keeps working unchanged, because the syntax never had to change.
Read-Only Properties
If you define only a getter and skip the @x.setter method entirely, the property becomes read-only. Any attempt to assign to it raises an AttributeError, which is useful for values that should be computed once and never overwritten from outside the class, such as an ID or a derived value like full_name above.
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
c = Circle(2)
print(c.area)
c.area = 100 # no setter defined for 'area'12.56636 AttributeError: can't set attribute