Layer 3 Session 26 โ€” Gate State & Interactivity

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.

population 54,000,000 changes over time

A value that is always recalculated โ€” never stored, never stale.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

A CountryExplorer stores both self.countries and a separately maintained self.country_count, updated by hand every time countries changes. What is the risk?

Any derived value stored separately from its source is a duplication risk โ€” the moment one code path updates countries but forgets to also update country_count, the two disagree, and nothing warns you.
Question 2 of 5

What is the fix for the country_count problem above, using what we already know about methods?

A computed method has no way to drift out of sync, because it recalculates the true answer from the actual source of truth (self.countries) every single time it is called โ€” there is nothing to forget to update.
Question 3 of 5

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.
Question 4 of 5

Should country_count be given a setter, e.g. explorer.country_count = 5, allowing it to be assigned directly?

A computed property's entire value is deriving safety from ALWAYS being recalculated from the real source of truth. Allowing it to be set directly would reintroduce the same "stored copy that can drift" problem this session solves.
Question 5 of 5

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))?

The test is: can this value be recalculated purely from other state that already exists? population cannot โ€” it IS the source of truth. country_count can โ€” it is 100% derivable from len(self.countries), making it a computed property, not independent state.

3. The Concept โ€” Computed Properties

STORED COPYcan drift outof syncCOMPUTEDalways correct,recalculated fresh

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.

Layer 3 gate: This is the last Layer 3 session. Deciding what is real state versus what should be computed on demand is a judgment call every remaining session assumes you can make correctly.

4. Lab

Lab objective: Identify and eliminate redundant stored state in CountryExplorer by converting it to @property-based computed values.

What you will build

A file called computed_lab.py.

Step-by-step instructions

1

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
2

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
3

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)
4

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
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

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"
Do not commit until you can answer out loud: "Why can country_count as a @property never drift out of sync, while the stored version could?"

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.

Question 1 of 5

A CountryExplorer stores both self.countries and a separately maintained self.country_count, updated by hand every time countries changes. What is the risk?

Any derived value stored separately from its source is a duplication risk โ€” the moment one code path updates countries but forgets to also update country_count, the two disagree, and nothing warns you.
Question 2 of 5

What is the fix for the country_count problem above, using what we already know about methods?

A computed method has no way to drift out of sync, because it recalculates the true answer from the actual source of truth (self.countries) every single time it is called โ€” there is nothing to forget to update.
Question 3 of 5

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.
Question 4 of 5

Should country_count be given a setter, e.g. explorer.country_count = 5, allowing it to be assigned directly?

A computed property's entire value is deriving safety from ALWAYS being recalculated from the real source of truth. Allowing it to be set directly would reintroduce the same "stored copy that can drift" problem this session solves.
Question 5 of 5

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))?

The test is: can this value be recalculated purely from other state that already exists? population cannot โ€” it IS the source of truth. country_count can โ€” it is 100% derivable from len(self.countries), making it a computed property, not independent state.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. 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?
  2. Why does @property choose to hide the fact that a value is being recalculated every single access, rather than making that obvious with parentheses?
  3. 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?
  4. 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.