Computed Properties
This is the Layer 3 gate. Some values should never be stored as their own piece of state โ they should be computed fresh, every time, from state that already exists.
A value that is always recalculated โ never stored, never stale.
1. Learning Objective
By the end of this session you will be able to:
- Identify a value that is redundant because it can always be derived from existing state
- Implement a computed value as a regular method
- Use the @property decorator to expose a computed value with attribute-style access
- Explain the bug risk of storing a derived value separately instead of computing it
- Decide, for a given value, whether it belongs as real state or as a computed property
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
A CountryExplorer stores both self.countries and a separately maintained self.country_count, updated by hand every time countries changes. What is the risk?
countries but forgets to also update country_count, the two disagree, and nothing warns you.What is the fix for the country_count problem above, using what we already know about methods?
What does the @property decorator change about calling a method like def total_population(self): return sum(c.population for c in self.countries)?
@property lets a method be READ using attribute syntax, without parentheses โ useful when a computed value conceptually feels like a property of the object (like .total_population) rather than an action being performed.Should country_count be given a setter, e.g. explorer.country_count = 5, allowing it to be assigned directly?
Which value is a better candidate to be stored as real state versus computed on demand: self.population (set by construction, changed by grow_population) or self.country_count (always equal to len(self.countries))?
3. The Concept โ Computed Properties
Stored derived state can silently go stale. A computed method always re-derives the true answer from the real source.
The redundant-state problem
Storing a value that can always be recalculated from other state creates a duplication risk: the two copies can drift out of sync the moment one is updated without the other.
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
self.country_count = len(countries) # RISKY โ a separate, stored copy
def add_country(self, country):
self.countries.append(country)
# Forgot to update self.country_count here!
# Now it silently disagrees with the real length.
explorer = CountryExplorer(countries=[])
explorer.add_country(Country(name="Kenya", region="Africa", population=54000000))
print(explorer.country_count) # 0 โ WRONG, still the stale initial value
print(len(explorer.countries)) # 1 โ the actual truth
The fix โ compute it, don't store it
A method that recalculates the value from the real source of truth every time it is called has nothing to forget to update โ it is always correct by construction.
class CountryExplorer:
def __init__(self, countries):
self.countries = countries # the ONLY source of truth
def country_count(self):
return len(self.countries) # always recalculated fresh, cannot drift
def add_country(self, country):
self.countries.append(country)
explorer = CountryExplorer(countries=[])
explorer.add_country(Country(name="Kenya", region="Africa", population=54000000))
print(explorer.country_count()) # 1 โ always correct, nothing to forget
@property โ attribute-style access for a computed value
When a computed value conceptually feels like a property of the object rather than an action, @property lets callers read it without parentheses, exactly like a stored attribute โ while still guaranteeing it is always freshly derived.
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
@property
def country_count(self):
return len(self.countries)
@property
def total_population(self):
return sum(c.population for c in self.countries)
explorer = CountryExplorer(countries=[
Country(name="Kenya", region="Africa", population=54000000),
Country(name="Peru", region="Americas", population=33000000),
])
print(explorer.country_count) # 2 โ no parentheses, reads like an attribute
print(explorer.total_population) # 87000000 โ freshly computed every access
When NOT to use a computed property
A value that is genuinely independent โ not derivable from other state โ belongs as real, stored state. population itself cannot be computed from anything else; it IS the source of truth, changed only through the controlled methods from Session 21.
4. Lab
What you will build
A file called computed_lab.py.
Step-by-step instructions
Create the file with the RISKY version that has stored, driftable derived state
# computed_lab.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
class RiskyExplorer:
def __init__(self, countries):
self.countries = countries
self.country_count = len(countries) # stored, can drift
self.total_population = sum(c.population for c in countries) # stored, can drift
def add_country(self, country):
self.countries.append(country)
# deliberately NOT updating country_count or total_population
Prove the drift bug happens
risky = RiskyExplorer(countries=[Country(name="Kenya", region="Africa", population=54000000)])
risky.add_country(Country(name="Peru", region="Americas", population=33000000))
print("Stored count (WRONG):", risky.country_count) # still 1
print("Real count:", len(risky.countries)) # 2
print("Stored population (WRONG):", risky.total_population) # still 54000000
Build the fixed version using @property
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def add_country(self, country):
self.countries.append(country)
@property
def country_count(self):
return len(self.countries)
@property
def total_population(self):
return sum(c.population for c in self.countries)
Prove the fixed version can never drift
explorer = CountryExplorer(countries=[Country(name="Kenya", region="Africa", population=54000000)])
explorer.add_country(Country(name="Peru", region="Americas", population=33000000))
print("Computed count:", explorer.country_count) # 2 โ correct
print("Computed population:", explorer.total_population) # 87000000 โ correct
Write a one-sentence comment for each attribute in CountryExplorer classifying it as STATE or COMPUTED
Connect this back to Session 20's STATE/FIXED annotation exercise.
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
computed_lab.py |
Created | Contrasts stored, driftable derived state with correct @property-based computed values. |
docs/sessions/session-26/index.html |
Created | This session document โ Layer 3 gate. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add computed_lab.py docs/sessions/session-26/index.html
git commit -m "session-26: replace stored derived state with @property computed values"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
A CountryExplorer stores both self.countries and a separately maintained self.country_count, updated by hand every time countries changes. What is the risk?
countries but forgets to also update country_count, the two disagree, and nothing warns you.What is the fix for the country_count problem above, using what we already know about methods?
What does the @property decorator change about calling a method like def total_population(self): return sum(c.population for c in self.countries)?
@property lets a method be READ using attribute syntax, without parentheses โ useful when a computed value conceptually feels like a property of the object (like .total_population) rather than an action being performed.Should country_count be given a setter, e.g. explorer.country_count = 5, allowing it to be assigned directly?
Which value is a better candidate to be stored as real state versus computed on demand: self.population (set by construction, changed by grow_population) or self.country_count (always equal to len(self.countries))?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Can you think of a value from an earlier session (Sessions 1โ21) that was accidentally stored as separate state when it could have been computed instead?
- Why does @property choose to hide the fact that a value is being recalculated every single access, rather than making that obvious with parentheses?
- Is population itself ever a good candidate to become a computed property? Why or why not, given what you know about its source of truth?
- What is the performance tradeoff of computing a value fresh every access instead of caching it โ when might that tradeoff actually matter?
10. What Breaks If This Knowledge Is Missing?
- The classic "shows the wrong total" bug: Nearly every "the displayed total doesn't match the actual list" bug in real software comes from exactly this pattern: a stored derived value that one code path forgot to update. This session directly immunizes you against it.
- The mock data layer (Layer 4): Session 28's CountryRepository will expose several computed values (counts, filtered subsets) โ built entirely on the @property pattern from this session.
- Testing derived values (Layer 5): Tests that assert on a count or a total (Session 33) are far simpler to write correctly against a computed property, since there is no separate "did you remember to update the stored copy" step to also test.
11. What We Learned
Python concept mastered: Computed properties โ eliminating redundant stored state by deriving values on demand with @property, guaranteeing they can never drift out of sync.
Unlocks: You can now correctly judge whether a value is genuine state or a derived computation โ the last Layer 3 skill before we start working with real (mock) data sources.
Next session: Session 27 โ Why Mock Data Matters. Layer 4 begins. We build against fake data before any real data source exists โ exactly like real engineering teams do.