Two things to notice — the missing brackets on the read, and the fact that nothing stores the area.
class Square:
def __init__(self, side):
self.side = side
@property
def area(self):
return self.side ** 2
square = Square(3)
print(square.area) # -> 9 no brackets, it is computed on the spot
square.side = 4
print(square.area) # -> 16 it cannot be stale, there is nothing stored
- Computed means it can never be out of date
If area were stored in __init__, changing side would leave it wrong until somebody remembered to recalculate. Because it is derived every time it is read, the two cannot disagree. That guarantee is the reason to reach for a property rather than an attribute.
- A setter turns assignment into a method call
Adding @area.setter would make `square.area = 25` run your code instead of storing a value — which is where validation belongs, because it then runs on every assignment rather than only in the constructor. Without a setter, the property is read-only and assigning raises AttributeError.
- The stored value hides behind an underscore
A property named celsius cannot also be the attribute that holds the number — the getter would call itself forever. The convention is to store it as _celsius and let the property be the public face, which is also why callers can never reach the unvalidated value by accident.
Store one number, not two. Everything else is derived from it — which means the fahrenheit conversion happens when someone reads, not when someone writes.
Make celsius a property backed by self._celsius, with a setter that validates. Make fahrenheit a property that computes from _celsius, with a setter that converts and assigns to self.celsius.
Have __init__ do `self.celsius = celsius` — assigning through the property means the validation lives in exactly one place and the constructor gets it for free. The fahrenheit setter should assign to self.celsius rather than self._celsius, for the same reason.
ABSOLUTE_ZERO = -273.15 class Temperature: def __init__(self, celsius): self.____ = celsius # through the setter, not _celsius @____ def celsius(self): return self._celsius @celsius.____ def celsius(self, value): if value < ABSOLUTE_ZERO: raise ValueError(...) self._celsius = value @property def fahrenheit(self): return self._celsius * ____ / ____ + 32 @fahrenheit.setter def fahrenheit(self, value): self.celsius = (value - 32) * ____ / ____
ABSOLUTE_ZERO = -273.15
class Temperature:
"""A temperature, stored in celsius."""
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < ABSOLUTE_ZERO:
raise ValueError(
f"{value} is below absolute zero ({ABSOLUTE_ZERO})"
)
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9
`self.celsius = celsius` runs the setter, so the validation is written once and applies to construction as well as to later assignment. Writing `self._celsius = celsius` instead would skip it, and Temperature(-300) would build happily — which is precisely the case that catches it. One rule, one place.
It converts and then assigns to the public property, so the absolute-zero check applies to fahrenheit too without being repeated. Three cases here test that rule from three directions and only one line implements it. Duplicated validation is duplicated opportunity to update one copy.
There is no attribute that could go stale, no synchronising step to forget, and no way for the two readings to disagree. The alternative — storing both and updating each on change — is more code and has a bug waiting in it the first time somebody adds a third way to set the value.
Do not reach for a property when a plain attribute will do; adding one 'just in case' is indirection with no payoff, and Python lets you convert an attribute into a property later without changing a single caller — that is the point of them. Note too that a property hides a computation behind something that looks free: if the getter runs a query or a long calculation, a caller in a loop will not suspect it, and a method with brackets is the honest signal. And for scientific work use a units library such as pint, which tracks dimensions and stops you adding a temperature to a length at all.
- Add kelvin as a third property and notice that the absolute-zero rule becomes 'not negative' when expressed in it.
- Make the object immutable — no setters at all, with .with_celsius(value) returning a new Temperature — and decide which design you would rather debug.
- Add __eq__ and think about whether 0°C and 32°F should compare equal, and what that means for floating-point tolerance.