Passing State Between Functions
When several functions need to work with the same evolving state, where should that state actually live? This session is about ownership, not just passing values around.
One shared object, passed deliberately between functions.
1. Learning Objective
By the end of this session you will be able to:
- Pass an object between functions and confirm mutations are visible to all of them (Session 05's reference lesson, applied)
- Decide when a function should own state versus receive it from a caller
- Move a piece of state up to a shared "owner" when multiple functions need to coordinate around it
- Explain the risk of two different functions each keeping their own separate copy of what should be one shared piece of state
- Refactor a menu's local state into a small class that owns it
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 function search(term) filters a CountryExplorer's countries and a separate function stats() reports on the SAME explorer. If both take explorer as a parameter, do they see the same countries?
Two separate functions each maintain their own local list of "loaded countries" instead of sharing one CountryExplorer. What risk does this create?
When should a function OWN a piece of state (create and hold it) versus RECEIVE it as a parameter from a caller?
You refactor a menu's ad-hoc local variables into a small MenuState class that owns them. What is the main benefit?
If a function receives an object and reassigns the LOCAL parameter name to point at a brand new object (e.g. explorer = CountryExplorer(countries=[])), does that affect the caller's original object?
3. The Concept โ State Ownership Between Functions
Mutating through a parameter changes the shared object. Reassigning the local name just points that one name elsewhere.
Shared state is visible everywhere it is referenced
This session applies Session 05's reference lesson directly to whole objects passed between functions โ a natural consequence of everything already learned, formalized as a design principle.
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def search(explorer, term):
return [c for c in explorer.countries if term.lower() in c.name.lower()]
def add_sample_data(explorer):
explorer.countries.append(Country(name="Kenya", region="Africa", population=54000000))
explorer = CountryExplorer(countries=[])
add_sample_data(explorer)
print(search(explorer, "ken")) # finds Kenya โ both functions see the SAME explorer
Mutation vs reassignment โ a subtle but important distinction
Mutating an object through a function parameter is visible to the caller. Reassigning what the local parameter name points to is NOT โ the caller's original reference is untouched.
def mutate_countries(explorer):
explorer.countries.append(Country(name="Ghana", region="Africa", population=31000000))
# visible to the caller โ same object, contents changed
def reassign_explorer(explorer):
explorer = CountryExplorer(countries=[])
# NOT visible to the caller โ this only rebinds the LOCAL name "explorer"
# to a brand new object; the caller's original variable is untouched
original = CountryExplorer(countries=[])
mutate_countries(original)
print(len(original.countries)) # 1
reassign_explorer(original)
print(len(original.countries)) # still 1 โ the reassignment inside the function had no effect here
Deciding who owns state
Local, private data that only one function cares about should be owned locally. Data that multiple functions need to coordinate around should be passed in explicitly, so everyone works from the same source of truth.
# Risky โ two functions each keep their OWN separate list, meant to represent the same thing
loaded_a = []
loaded_b = []
def load_into_a():
loaded_a.append("Kenya")
def report_from_b():
print(loaded_b) # never sees "Kenya" โ these are two DIFFERENT lists!
# Better โ one owner, shared and passed explicitly
class Loader:
def __init__(self):
self.loaded = []
def load(self, name):
self.loaded.append(name)
def report(self):
print(self.loaded) # always sees everything loaded through THIS instance
Bundling scattered local state into a small owning class
When a menu (Session 22) accumulates several loose local variables that all need to travel together between functions, wrapping them in a small class โ exactly like Session 12's original motivation โ gives them a clear, shared owner.
4. Lab
What you will build
A file called state_passing_lab.py.
Step-by-step instructions
Create the file with Country and CountryExplorer
# state_passing_lab.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
Write two functions that both operate on the same passed-in explorer
def search(explorer, term):
return [c for c in explorer.countries if term.lower() in c.name.lower()]
def add_sample_data(explorer):
explorer.countries.append(Country(name="Kenya", region="Africa", population=54000000))
explorer = CountryExplorer(countries=[])
add_sample_data(explorer)
print(search(explorer, "ken"))
Demonstrate the mutation-vs-reassignment distinction explicitly
Predict the output of each print BEFORE running.
def mutate_countries(exp):
exp.countries.append(Country(name="Ghana", region="Africa", population=31000000))
def reassign_explorer(exp):
exp = CountryExplorer(countries=[]) # only rebinds the local name
mutate_countries(explorer)
print("After mutate:", len(explorer.countries)) # 2
reassign_explorer(explorer)
print("After reassign attempt:", len(explorer.countries)) # still 2
Write a MenuState class that owns previously-scattered local variables
class MenuState:
def __init__(self, explorer):
self.explorer = explorer
self.last_search_term = None
self.action_count = 0
def run_search(self, term):
self.last_search_term = term
self.action_count += 1
return search(self.explorer, term)
Pass one MenuState object between two functions and confirm both see the same data
def report(state):
print("Last search:", state.last_search_term)
print("Actions taken:", state.action_count)
state = MenuState(explorer)
state.run_search("gh")
report(state) # sees the search performed by run_search, because it's the SAME state object
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
state_passing_lab.py |
Created | Demonstrates shared reference passing, mutation vs reassignment, and a MenuState owner class. |
docs/sessions/session-25/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add state_passing_lab.py docs/sessions/session-25/index.html
git commit -m "session-25: pass shared state explicitly between functions via one owning object"
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 function search(term) filters a CountryExplorer's countries and a separate function stats() reports on the SAME explorer. If both take explorer as a parameter, do they see the same countries?
Two separate functions each maintain their own local list of "loaded countries" instead of sharing one CountryExplorer. What risk does this create?
When should a function OWN a piece of state (create and hold it) versus RECEIVE it as a parameter from a caller?
You refactor a menu's ad-hoc local variables into a small MenuState class that owns them. What is the main benefit?
If a function receives an object and reassigns the LOCAL parameter name to point at a brand new object (e.g. explorer = CountryExplorer(countries=[])), does that affect the caller's original object?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Before running the lab, did you correctly predict that reassign_explorer would have no effect on the caller's explorer? What was your reasoning?
- Can you think of a case in the earlier labs where you accidentally relied on (or were confused by) this exact mutation-vs-reassignment distinction?
- Why does bundling loose local variables into MenuState make it easier to eventually add a THIRD function that also needs last_search_term?
- How does this session's "one owner, passed explicitly" principle compare to Session 20's warning about uncontrolled shared mutation? Are they in tension, or do they work together?
10. What Breaks If This Knowledge Is Missing?
- Silently divided state: If two parts of a program each keep their own copy of what should be one shared truth, they will eventually disagree โ one shows stale data while the other has the update, and there is no way to tell which one is "correct" without deep debugging.
- Computed properties (Session 26): The next session (the Layer 3 gate) asks: should a value be stored as state, or computed fresh each time from other state? Understanding who owns data and how it flows is required before that question makes sense.
- Sharing data with a repository (Layer 4): Session 28 introduces a CountryRepository that many different parts of the application share explicitly, exactly the pattern this session establishes.
11. What We Learned
Python concept mastered: State ownership between functions โ reference sharing, the mutation vs reassignment distinction, and bundling scattered local state into one owning object.
Unlocks: You can now design who owns a given piece of state and pass it deliberately, instead of accidentally creating divided, out-of-sync copies.
Next session: Session 26 โ Computed Properties. Layer 3 gate. We ask whether a value should be stored as state at all, or computed on demand from other state.