Python @property: Getters, Setters, and Computed Attributes
Imagine a thermostat on your wall. You set the temperature to 72°F and the system heats your house. But what if someone sets it to 5000°F? The house wouldn't literally reach that temperature, but your heating system would run forever trying. A smart thermostat would reject that value immediately.
Python's @property decorator gives your classes that same intelligence. It lets you control what happens when someone reads, changes, or deletes an attribute — without changing how the attribute looks from the outside.
By the end of this tutorial, you'll know how to validate data going into your objects, create computed attributes that update automatically, and write classes that are both easy to use and impossible to misuse.
Why Not Just Use Regular Attributes?
In Python, you can set any attribute to any value. There are no guardrails by default. That's fine for simple scripts, but it becomes a problem when bad data can break your program.
One approach is to use getter and setter methods like get_temperature() and set_temperature(). This works, but it's clunky. Every user of your class has to remember to call methods instead of using simple attribute syntax.
class Thermostat:
def __init__(self, temp):
self._temp = temp
def get_temperature(self):
return self._temp
def set_temperature(self, value):
if value < -459 or value > 1000:
raise ValueError('Invalid temp')
self._temp = value
# Awkward to use
t = Thermostat(72)
print(t.get_temperature())
t.set_temperature(80)class Thermostat:
def __init__(self, temp):
self.temperature = temp
@property
def temperature(self):
return self._temp
@temperature.setter
def temperature(self, value):
if value < -459 or value > 1000:
raise ValueError('Invalid temp')
self._temp = value
# Natural to use
t = Thermostat(72)
print(t.temperature)
t.temperature = 80With @property, your class users write t.temperature = 80 just like a normal attribute. But behind the scenes, your validation code runs automatically. Best of both worlds.
How Does the @property Decorator Work?
The @property decorator turns a method into a "getter" — code that runs whenever you read an attribute. The method name becomes the attribute name.
Notice the naming convention. The actual data lives in self._radius (with an underscore). The property is named radius (without the underscore). The underscore signals "this is internal — don't touch it directly."
Without a setter, the property is read-only. If you try to assign to it, Python raises an AttributeError.
How Do You Add a Setter to a Property?
To allow assignment, define a setter using @property_name.setter. The setter receives the new value and decides what to do with it — store it, validate it, transform it, or reject it.
Notice that __init__ uses self.temperature = temp (without underscore). This means the setter runs during initialization too. Your object is validated from the very first moment it exists.
What Are Computed and Read-Only Properties?
Some attributes aren't stored anywhere. They're calculated on the fly from other data. Think of a rectangle: you store width and height, but the area is just width * height. There's no reason to store the area separately.
Computed properties always return the latest value because they recalculate every time you access them. You never have stale data.
Here's a practical example: a BankAccount that derives its status from the balance.
When Should You Use @property?
Use @property when you need to add behavior to attribute access. Here are the most common scenarios:
Validation: Reject bad data before it enters your object. Great for ages, prices, email addresses, or any value with constraints.
Computed values: Derive one attribute from others (area from width/height, full name from first/last). No need to store redundant data.
Read-only attributes: Expose data that should never be changed from outside the class, like an object's creation timestamp.
Backward compatibility: If you started with a plain attribute and later need to add validation, @property lets you do it without changing the class's public interface.
Practice Exercises
Create a class Person with an __init__ that takes a name parameter and stores it in self._name. Add a read-only @property called name that returns self._name. Then create a Person with the name 'Alice' and print their name.
Create a class Student with a name (regular attribute) and an age property. The age setter should raise a ValueError with the message 'Age must be between 0 and 150' if the value is outside that range. Use the setter inside __init__. Then create a Student('Bob', 20), print their age, update it to 21, and print again.
This class causes infinite recursion when you create an instance. Find and fix the bug so that creating Product('Widget', 9.99) prints the price correctly.
Create a class Person with first_name and last_name as regular attributes. Add a read-only computed property full_name that returns them joined with a space. Create Person('John', 'Doe'), print full_name, then change last_name to 'Smith' and print full_name again.
Read the code carefully and predict exactly what gets printed. Think about which setter calls succeed and which raise errors.
Refactor this class to use @property instead of explicit getter/setter methods. The get_speed and set_speed methods should be replaced with a speed property. Keep the validation logic. Then create a Car(60), print speed, set it to 100, and print again.