Every language with private fields teaches you to write get_x() and set_x() up front, in case you need validation later. Python does not need that, and the reason is worth understanding rather than memorising.
The habit Python does not need
# the Java habit, which Python does not need
class TemperatureJava:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
self._celsius = value
t = TemperatureJava(20)
t.set_celsius(25)
print(t.get_celsius())
It prints 25. Six lines of ceremony that do nothing except make the call sites uglier.
Start with a plain attribute
# in Python you start with a plain attribute
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t2 = Temperature(20)
t2.celsius = 25
print(t2.celsius)
It prints 25. Same behaviour, no ceremony.
The objection is obvious: what happens when you need validation? In a language without properties the answer is “you change every caller”, which is why people write getters defensively. In Python the answer is that you do not.
Add @property later, and nothing outside changes
# and add @property later, without changing a single caller
class Temperature2:
def __init__(self, celsius):
self.celsius = celsius # goes through the setter below
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"{value} is below absolute zero")
self._celsius = value
t3 = Temperature2(20)
t3.celsius = 25
print(t3.celsius)
try:
t3.celsius = -300
except ValueError as err:
print(type(err).__name__ + ':', err)
It prints:
25
ValueError: -300 is below absolute zero
t3.celsius = 25 still reads like an attribute assignment because it is one, syntactically. The property intercepts it. Every existing caller keeps working unchanged. That is the whole argument: you can defer the decision until you have a reason, because making it later costs nothing.
Note that __init__ assigns self.celsius, not self._celsius. That routes construction through the setter, so an invalid value is rejected at creation too rather than only on later assignment.
Computed properties
The other use, and the more common one: a value derived from others, so it can never go stale.
# a computed property: derived, never stored, never stale
class Temperature3:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
t4 = Temperature3(100)
print(t4.fahrenheit)
t4.celsius = 0
print(t4.fahrenheit)
It prints:
212.0
32.0
Store fahrenheit as a real attribute and you have two sources of truth that will disagree the first time someone changes one of them. Computed, it cannot.
Read-only for free
Leave out the setter and assignment fails:
try:
t4.fahrenheit = 50
except AttributeError as err:
print(type(err).__name__ + ':', err)
It prints:
AttributeError: property 'fahrenheit' of 'Temperature3' object has no setter
That message is Python 3.11 and later. Older versions say can't set attribute, which is less helpful and the same thing.
What to remember
-
Start with a plain attribute. Always.
-
Add
@propertywhen you actually need validation or computation — callers never notice. -
Assign through the property in
__init__so construction is validated too. -
A property with no setter is read-only, which is the cheapest way to protect a derived value.
The one thing not to do is wrap every attribute in a property “just in case”. That is the getter habit with better syntax, and it costs the same clarity for the same nothing.