The Python property decorator turns a method into something you read like a plain attribute. Put @property above a method, and obj.name runs that method with no parentheses. Add @name.setter when you want to check values on assignment:
class Account:
@property
def balance(self):
return self._balance
Every output below came from Python 3.12.5 in Command Prompt on Windows 11.
In this tutorial, we’ll build read-only, validated, computed, deletable and cached properties, look at the property() function form, and fix common mistakes.
Create a read-only property with @property
A property with only a getter is read-only. The value lives in _balance; the property formats it:
class Account:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # leading underscore = internal
@property
def balance(self):
return f"${self._balance:,.2f}"
acct = Account("Emily Carter", 2500)
print(acct.balance) # no parentheses
try:
acct.balance = 9999
except AttributeError as err:
print("AttributeError:", err)
Output:
$2,500.00
AttributeError: property 'balance' of 'Account' object has no setter

The underscore is a convention, not protection. Private variables and data hiding in Python explains what it does and doesn’t stop.
Validate values with a @property.setter
The setter must have the same name as the getter. Because __init__ assigns self.salary, even the first value gets checked:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary # uses the setter
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, value):
if value < 0:
raise ValueError("salary can't be negative")
self._salary = round(value, 2)
emp = Employee("James Lee", 72000)
emp.salary = 75500.456
print(emp.name, emp.salary)
for bad in (-100, "lots"):
try:
emp.salary = bad
except (ValueError, TypeError) as err:
print(type(err).__name__ + ":", err)
Output:
James Lee 75500.46
ValueError: salary can't be negative
TypeError: '<' not supported between instances of 'str' and 'int'

Callers keep dot syntax while the class guards its state. See data encapsulation in Python for the bigger picture, and the __init__ method in Python for how constructors fit in.
Create a computed property in Python
A computed property stores nothing. It works out the answer from other attributes on every read, so it never goes stale:
class Cart:
TAX_RATE = 0.0825 # Texas state + local
def __init__(self):
self.items = []
@property
def subtotal(self):
return sum(price * qty for _, price, qty in self.items)
@property
def total(self):
return round(self.subtotal * (1 + self.TAX_RATE), 2)
cart = Cart()
cart.items.append(("Coffee beans", 14.99, 2))
print(f"{cart.subtotal:.2f} -> {cart.total:.2f}")
cart.items.append(("Mug", 9.50, 1))
print(f"{cart.subtotal:.2f} -> {cart.total:.2f}") # recalculated
Output:
29.98 -> 32.45
39.48 -> 42.74

Prefer this over storing total as an attribute you must remember to refresh. Python dataclasses work with properties too.
How do you delete a property with @deleter?
A deleter runs when someone writes del obj.name. It’s handy for cleanup:
class Session:
_token = "abc123"
@property
def token(self):
return self._token
@token.deleter
def token(self):
print("Logging out")
self._token = None
s = Session()
del s.token
print(s.token)
Output:
Logging out
None
Without a deleter, that del raises AttributeError.
Cache slow results with functools.cached_property
cached_property runs the method once and stores the result on the instance, so you skip the work on later reads:
from functools import cached_property
class Report:
def __init__(self, sales):
self.sales = sales
@cached_property
def summary(self):
print(" (computing...)") # imagine a slow query here
return f"{len(self.sales)} sales, ${sum(self.sales):,.2f}"
r = Report([120.50, 89.99, 240.00])
print(r.summary)
print(r.summary) # cached, no message
del r.summary # clear the cache
print(r.summary)
Output:
(computing...)
3 sales, $450.49
3 sales, $450.49
(computing...)
3 sales, $450.49

del r.summary it runs again.Use it for values that don’t change. It won’t work on classes that use __slots__ to save memory.
Use the property() function instead of the decorator
@property is shorthand for the built-in property(fget, fset, fdel, doc), which you’ll meet in older code:
class Thermostat:
def __init__(self, fahrenheit):
self._f = fahrenheit
def get_celsius(self):
return round((self._f - 32) * 5 / 9, 1)
celsius = property(get_celsius) # same as @property
print(Thermostat(72).celsius)
Output:
22.2
Both forms behave the same; I prefer the decorator. To set attributes by name at runtime, see setattr() and getattr() in Python.
Common @property mistakes and how to fix them
The classic bug is returning self.price inside the price getter, so the property calls itself:
class Product:
@property
def price(self):
return self.price # bug: calls itself
try:
Product().price
except Exception as err:
print(type(err).__name__ + ":", err)
Output:
RecursionError: maximum recursion depth exceeded
Store the value as self._price instead, and give the setter the getter’s exact name.
When should you use @property instead of a plain attribute?
I start with plain attributes. Switch to a property when you need validation, a computed value or read-only access; callers won’t need to change a line. For slow work or arguments, use a method; Python property vs methods compares both.
| Need | Use |
|---|---|
| Read-only or formatted value | @property |
| Check values on assignment | @x.setter |
| Expensive value computed once | @cached_property |
| Needs arguments | Regular method |
Ready to keep going with classes? These are good next reads:
- Python class decorators with examples
- Class method vs static method in Python
- The __dict__ attribute in Python classes
- Build a simple OOP project in Python
Frequently asked questions
What does the @property decorator do in Python?
It turns a method into a managed attribute, so obj.name calls the method without parentheses.
How do I make a read-only property in Python?
Define only the @property getter. Assigning to it then raises AttributeError.
Why does my property cause a RecursionError?
The getter or setter uses the property’s own name. Store the value under another name like self._price.
What’s the difference between property and cached_property?
A property runs its method on every read; cached_property runs once and stores the result until you delete it.
Can a property take arguments?
No. A getter only receives self. If you need arguments, write a regular method.
Where is property() documented?
See property() in the Python docs.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn