How to Use the @property Decorator in Python

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
Command Prompt output of a Python read-only property returning a formatted balance and raising AttributeError on assignment
Reading the property works, while assigning to it raises AttributeError because there’s 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'
Command Prompt output of a Python property setter rounding a salary and rejecting a negative number and a string
The setter rounds the valid salary to two decimals and rejects the negative value and the string.

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
Command Prompt output of Python computed properties recalculating a cart subtotal and total with sales tax
Adding a mug changes both the subtotal and the total without any extra update call.

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
Command Prompt output of Python functools.cached_property computing a report once, returning the cached value, then recomputing after del
Only the first read prints the computing message; after 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.

NeedUse
Read-only or formatted value@property
Check values on assignment@x.setter
Expensive value computed once@cached_property
Needs argumentsRegular method

Ready to keep going with classes? These are good next reads:

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.