Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Python Properties and Managed Attributes

Learn how Python properties manage attributes and when functools.cached_property is useful for reusable per-instance computed values.

Levels: Intermediate ~26 min read

Intermediate

A property lets a class present a method-backed value through ordinary attribute syntax. Reading, assigning, or deleting the attribute can run code while the public interface still looks like obj.value.

Turn a method into an attribute with @property

A property lets a class present a method-backed value through ordinary attribute syntax. Reading the property runs its getter.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

circle = Circle(4)
print(circle.diameter)

The getter runs when circle.diameter is read.

A property can calculate a value

A property does not have to store its result. A getter can calculate a value from other instance state while callers still use attribute syntax.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

box = Rectangle(6, 3)
print(box.area)

area behaves like a read-only attribute even though its value is computed by a method.

Make a property writable with a setter

A getter alone does not define how assignment should work. Add @name.setter when assignments need validation, conversion, or another controlled behavior.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("temperature is below absolute zero")
        self._celsius = value

reading = Temperature(20)
reading.celsius = 25
print(reading.celsius)

The setter validates an assignment before updating the backing attribute.

Use a backing attribute

A common pattern is to expose a public property name and keep the stored value under another instance attribute such as _name. The underscore communicates that the storage detail is internal.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class User:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

user = User("Mina")
print(user.name)

name is the public property; _name is the instance attribute used for storage.

Add deletion with a property deleter

A property can define a deleter with @name.deleter. Then del obj.name invokes that function, which decides what deletion means for the managed state.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Session:
    def __init__(self, token):
        self._token = token

    @property
    def token(self):
        return self._token

    @token.deleter
    def token(self):
        self._token = None

session = Session("abc123")
print(session.token)
del session.token
print(session.token)

The deleter changes the managed state when del session.token is executed.

A setter and deleter keep the same property name

The getter, setter, and deleter all use the same property name. The decorator form attaches the corresponding accessor to the property.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Counter:
    def __init__(self, value=0):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = int(new_value)

    @value.deleter
    def value(self):
        self._value = 0

counter = Counter(5)
counter.value = "8"
print(counter.value)
del counter.value
print(counter.value)

All three access paths—read, assignment, and deletion—share the same property name.

Properties preserve a simple public interface

A property is useful when a value should look like ordinary attribute data to callers but needs computed behavior, validation, conversion, or controlled deletion behind the scenes.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, amount):
        if amount < 0:
            raise ValueError("balance cannot be negative")
        self._balance = amount

account = Account(100)
account.balance = 150
print(account.balance)

Callers use account.balance consistently even though reads and writes execute methods.

Property objects and the descriptor connection

A property object implements the descriptor behavior that lets Python manage attribute access. You do not need to implement the descriptor protocol to use properties; @property provides the ready-made mechanism.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Profile:
    def __init__(self, username):
        self._username = username

    @property
    def username(self):
        return self._username

profile = Profile("sam")
print(type(type(profile).username).__name__)
print(profile.username)

The class stores a property object under username; accessing profile.username invokes that property’s getter.

Choose a property when attribute syntax communicates the right abstraction

Use a property when callers should think of the result as an attribute. Use a normal method when the operation is naturally an action, takes meaningful arguments, or is better expressed as a call.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Product:
    def __init__(self, price):
        self.price = price

    @property
    def discounted_price(self):
        return self.price * 0.9

    def apply_discount(self, rate):
        return self.price * (1 - rate)

product = Product(100)
print(product.discounted_price)
print(product.apply_discount(0.2))

An argument-free derived value reads naturally as a property; an operation that accepts a rate is clearer as a method.

Cache an expensive computed attribute per instance

For a computed value that is expensive to produce and reused often, functools.cached_property turns a method into an attribute whose value is calculated on the first lookup and stored on that instance. Later lookups read the stored value rather than calling the method again. Unlike an ordinary @property getter, it does not automatically recalculate on every access.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from functools import cached_property

class Report:
    def __init__(self, values):
        self._values = tuple(values)
        self.computations = 0

    @cached_property
    def total(self):
        self.computations += 1
        return sum(self._values)

first = Report([2, 3, 4])
second = Report([10])
print(first.total, first.computations)
print(first.total, first.computations)
print(second.total, second.computations)

The first report calculates its total once, reuses it on the next access, and the second report computes an independent value.

The stored value belongs to one instance; it is not a shared cache keyed by function arguments. This is useful when a result depends on stable instance data, such as parsed metadata or a summary of an immutable collection.

Refresh or replace the cached value

The cached result can become stale if the data it depends on changes. Delete the instance attribute with del obj.name to let the descriptor calculate it again. Assignment to that same name is also allowed and takes precedence over the descriptor, so a cached property can be temporarily replaced with an instance value.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from functools import cached_property

class Inventory:
    def __init__(self, items):
        self.items = list(items)
        self.computations = 0

    @cached_property
    def count(self):
        self.computations += 1
        return len(self.items)

inventory = Inventory(["pen"])
print(inventory.count, inventory.computations)
inventory.items.append("book")
print(inventory.count, inventory.computations)
inventory.count = 99
print(inventory.count)
del inventory.count
print(inventory.count, inventory.computations)

After the list changes, the cached count stays at 1. Assignment overrides it with 99; deleting that attribute makes the next read recompute the count as 2.

There are practical limits: @cached_property needs an instance with a mutable __dict__, so it does not work on a slot-only class that omits __dict__. Since Python 3.12 it also has no internal lock to guarantee the getter runs only once during concurrent first access. Use it when an occasional repeated calculation is safe, or provide your own synchronization if needed.

Quick check

The cached value has already been read once. What does the final expression print?

from functools import cached_property

class Sample:
    def __init__(self):
        self.calls = 0

    @cached_property
    def value(self):
        self.calls += 1
        return self.calls + 5

sample = Sample()
sample.value
print()

Quick check

After the input list changes, which expression makes the cached count calculate again?

from functools import cached_property

class Data:
    def __init__(self):
        self.values = [1]
        self.calls = 0

    @cached_property
    def count(self):
        self.calls += 1
        return len(self.values)

data = Data()
data.count
data.values.append(2)
del data.count
print()

Key takeaways

  • Use @property to expose a method through attribute-reading syntax.
  • Use @name.setter to control assignments and @name.deleter to control deletion.
  • A backing attribute such as _value can store the state managed by a property.
  • Properties provide a managed-attribute interface without requiring callers to change from attribute syntax to method calls.
  • Use a normal method when the operation is better understood as an action or needs meaningful arguments.
  • cached_property computes a value on the first lookup and stores it on that instance for reuse.
  • Delete a cached attribute to recalculate after its inputs change; assignment can override the cached value.
  • cached_property needs a mutable instance __dict__; its getter is not guaranteed to run only once under concurrent first access.
Was this page helpful?

Mark this learning resource complete to track your learning progress.