Blog

@property Instead of Getters and Setters

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 @property when 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.