Skip to main content

Properties: Computed Attributes

James opens the Note class from Chapter 60. Everything works: repr, eq, iter, hash. But there is a problem he has been ignoring since Chapter 58. He changes the body of a note:

note = Note("Warehouse Tips", "Keep aisles clear", author="James")
print(note.word_count) # 3
note.body = "Keep aisles clear and label every shelf"
print(note.word_count) # Still 3 -- wrong!

Output:

3
3

The word_count is a stored field, set once during __init__. When the body changes, the count stays stale. The object lies.

"The count should compute itself," Emma says. "Like a live dashboard. You do not store yesterday's inventory count and hope someone remembers to update it. The dashboard reads the shelves and calculates the count on demand."


If you're new to programming

An attribute is a value attached to an object, accessed with a dot: note.title. A property is an attribute that runs a method behind the scenes to compute its value. From the outside, note.word_count looks the same whether it is a stored field or a property. The difference is internal: a property always returns a fresh value.

If you've coded before

Python's @property decorator turns a method into a descriptor. Accessing obj.x calls x.fget(obj) under the hood. You can also define @x.setter and @x.deleter for write and delete behavior. Properties are Python's version of getters and setters, but with attribute-access syntax instead of method-call syntax.


The Stale Field Problem

Look at the __init__ from Chapter 58:

class Note:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
self.title = title
self.body = body
self.word_count = len(body.split()) # ← stored once
self.author = author
self.is_draft = True
self.tags: list[str] = []

self.word_count = len(body.split()) runs once, at creation time. If body changes later, word_count is stuck at the old value.

This is the stale field problem: storing a value that should be derived from another value.


The @property Fix

Replace the stored field with a method that computes the value on demand:

class Note:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
self.title = title
self.body = body
# word_count is no longer stored here
self.author = author
self.is_draft = True
self.tags: list[str] = []

@property
def word_count(self) -> int:
return len(self.body.split())

Now test it:

note = Note("Warehouse Tips", "Keep aisles clear", author="James")
print(note.word_count)
note.body = "Keep aisles clear and label every shelf"
print(note.word_count)

Output:

3
7

The count updates automatically because it computes from self.body every time you access it.

The Key Insight: Access Stays the Same

Before the change: note.word_count (accessing a stored field). After the change: note.word_count (accessing a property).

The calling code is identical. No parentheses needed. No method call syntax. This is why properties are powerful: you can change the internal implementation without changing the interface. Every line of code that uses note.word_count keeps working.


When to Use @property

ScenarioUse @property?Why
Value computed from other attributesYesStays synchronized automatically
Value that never changes after creationNo, use a stored fieldNo stale-data risk
Expensive computation (database query, API call)CarefullyRuns every time you access it; consider caching
Simple data storage (title, author)NoNo computation needed

The rule: if the value depends on other attributes that can change, make it a property. If the value is independent, store it.


Adding a Setter (Brief)

Sometimes you want to control how a value is set. A setter runs validation when someone assigns to the property:

class Note:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
self._title = title # ← underscore: internal storage
self.body = body
self.author = author
self.is_draft = True
self.tags: list[str] = []

@property
def title(self) -> str:
return self._title

@title.setter
def title(self, value: str) -> None:
stripped = value.strip()
if not stripped:
raise ValueError("Title cannot be empty")
self._title = stripped
note = Note("  Warehouse Tips  ", "Keep aisles clear")
print(note.title)
note.title = "Updated Tips"
print(note.title)
note.title = " " # raises ValueError

Output:

  Warehouse Tips  
Updated Tips
ValueError: Title cannot be empty

The pattern: store the real value in self._title (underscore prefix signals "internal"). The @property getter returns it. The @title.setter validates before storing.

Deleters

Python also supports @title.deleter for controlling del note.title. This is rarely needed. We mention it for completeness but will not use it in SmartNotes.


PRIMM-AI+ Practice: Property Prediction

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Read this class and predict the output of the test code below it. Write your prediction and a confidence score from 1 to 5.

class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height

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

r = Rectangle(3.0, 4.0)
print(r.area)
r.width = 5.0
print(r.area)
print(type(r.area))
Check your prediction
12.0
20.0
<class 'float'>

r.area computes from the current width and height. When width changes to 5.0, area returns 5.0 * 4.0 = 20.0. The property returns a float, not a method object, because @property makes Python call the method automatically.

Run

Press Shift+Tab to exit Plan Mode.

Create a file called property_practice.py. Write the Rectangle class above. Add a @property called perimeter that returns 2 * (self.width + self.height). Run it and verify that changing width updates both area and perimeter.

Investigate

In Claude Code, type:

/investigate @property_practice.py

Ask: "What happens if I try to assign to r.area = 100? Why does Python raise an error? What would I need to add to allow assignment?"


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Audit Fields for Property Candidates

Here is the SmartNotes Note class. Identify every field
that should be a @property instead of a stored field.
For each one, explain why it is a candidate (does it
depend on other fields that can change?) and show the
@property implementation.

class Note:
def __init__(self, title: str, body: str,
author: str = "Anonymous") -> None:
self.title = title
self.body = body
self.word_count = len(body.split())
self.author = author
self.is_draft = True
self.tags: list[str] = []

Read the AI's analysis. It should identify word_count as the primary candidate. If it suggests making is_draft a property, evaluate whether that makes sense (it does not; is_draft is independent state, not derived).

What you're learning: You are using the AI to audit your class design for stale-field risks, then evaluating its recommendations against the decision rule (depends on changeable attributes? = property).

Prompt 2: Property vs Method Tradeoff

When should I use @property vs a regular method in Python?
Give me three examples where @property is better and three
where a regular method is better. Explain the tradeoff
in terms of caller expectations and computation cost.

What you're learning: Properties look like data access, so callers expect them to be cheap. If computation is expensive (database lookup, network call), a regular method signals "this takes time." The AI helps you internalize where the boundary lies.

Prompt 3: Property for Your Domain

I work in [describe your professional domain: logistics,
healthcare, education, finance, etc.]. Give me a class
from my domain with two stored fields that should be
@property instead. Show the before (stored) and after
(property) versions and explain the stale-data risk.

What you're learning: You are transferring the property pattern from SmartNotes to your own professional context. The AI adapts the examples, but you evaluate whether its property candidates genuinely depend on mutable state.


James looks at the refactored Note class. "So word_count is not a field anymore. It is a method pretending to be a field."

"Pretending is the wrong word," Emma says. "It is a computed attribute. Like a live dashboard in your warehouse. When someone asks 'how many units on shelf B4?' the dashboard does not recall a number from this morning. It counts the units right now. That is what @property does. It counts right now."

"I had a dashboard that cached yesterday's numbers once," James says. "Nobody noticed for two days. We shipped partial orders because the count was wrong."

Emma nods. "Properties have the same risk in reverse. If the computation is expensive and you access it in a loop, you run it hundreds of times. That is why the rule matters: cheap computations are properties, expensive computations are methods. word_count splits a string and counts words. That is cheap. A database query is not."

"So properties compute values. But what about functions that belong to the class but do not need self? I have a note_from_dict() function sitting outside the class."

"That is next lesson. @staticmethod and @classmethod bring those functions home."