A property lets you write a method that looks like a regular attribute from the outside. Useful when you want to compute a value on the fly, or run some logic when an attribute is read or written.
A computed attribute
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
r = Rectangle(3.0, 4.0)
print(r.area) # 12.0 — looks like an attribute, but it's computed
Two things to notice:
@propertyon the line above the method makes it act like an attribute.- The caller writes
r.area(no parentheses), even though it’s a method underneath.
Whenever you’d write a no-argument getter — r.get_area() — consider a @property instead. It reads more naturally.
Why not just store it?
You could just compute the area in __init__:
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
self.area = width * height
The catch — if you later change width or height, area doesn’t update:
r = Rectangle(3.0, 4.0)
r.width = 10.0
print(r.area) # 12.0 — stale!
A property always recomputes when read:
r.width = 10.0
print(r.area) # 40.0 — fresh
When the value depends on others, make it a property.
Read-only attributes
A property by itself is read-only — you can’t assign to it:
r = Rectangle(3.0, 4.0)
r.area = 99.0 # AttributeError: property 'area' has no setter
This is a feature. It signals to callers: “you can’t change this directly”.
Adding a setter
If you want a property you can also write to, add a setter:
class Temperature:
def __init__(self, celsius: float) -> None:
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 30 # uses the setter, validates
print(t.fahrenheit) # 86.0
t.celsius = -300 # ValueError
The setter:
- Is decorated with
@celsius.setter— the property’s name dotsetter. - Has the same name as the property.
- Receives the assigned value as a parameter.
This pattern lets you validate values whenever they’re set, without callers having to remember a special method.
Sub-pattern — _name for the storage, name for the property
Notice we used self._celsius to store the value and celsius as the property. The underscore is the convention: “the real storage is private; access goes through the property”.
This is so common that you’ll see this pattern in every Python codebase that uses properties.
When to use properties
- The attribute is computed from others
- You want to validate values when they’re written
- You want to transform values (e.g. store internally as cents, expose as dollars)
- You want a read-only attribute
Skip the property when a plain attribute works. Don’t add ceremony for no reason.
A subtle gotcha — properties on dataclasses
We’ll meet dataclasses in lesson 6. Properties on dataclasses are tricky because dataclasses also generate attribute logic. If you need a computed value on a dataclass, the cleanest approach is usually a method or a @cached_property (from functools) — not a regular @property.
For most beginner use cases, you won’t hit this — but the warning is here so you don’t waste time later.
What’s next
You can shape how attributes look from the outside. Next, inheritance — building one class on top of another.