Unit 06 · lesson
Decomposition, Scope, and Program Design
Core path: 30 minutes
How do you decide what should become a function without turning the program into fifty tiny fragments?
This is the point where functions stop being syntax and start becoming architecture.
The question is no longer:
Can I put these lines under
def?
The better question is:
What responsibility does this part of the program own, what information crosses its boundary, and can another developer understand that boundary from the name?
Start with responsibilities, not line count
Imagine a system monitor that does all of this in one block:
ask for device data
convert input
check battery
check temperature
count warnings
calculate average
print device report
print final summary
The code may be only sixty lines long and already be difficult to reason about because several different jobs are mixed together.
Possible responsibilities include:
collect data
determine status
calculate totals
display report
Breaking a larger problem into smaller responsibilities is decomposition.
It is not the same thing as cutting every five lines into a function.
A function creates an interface to behavior
Consider:
status = determine_status(battery, temperature)
A caller needs to know:
inputs → battery, temperature
output → status
It does not need to reread every internal condition each time it uses the function.
That is abstraction: hiding implementation detail behind a simpler interface that still exposes the information the caller needs.
Abstraction is useful only when the interface is meaningful. Naming a giant mystery block do_everything() did not really solve the problem.
Write the contract before the implementation gets clever
For a function such as:
def determine_status(battery, temperature):
...
write the contract in plain language:
NAME
Determine the status of one device.
INPUTS
battery: numeric percentage
temperature: numeric reading
RETURNS
one status string
SIDE EFFECTS
none
That last line matters.
If the function also prints menus, edits a file, and asks the user for input, its responsibility is much harder to isolate and test.
Scope creates a real boundary
def create_message():
message = "System ready."
create_message()
print(message)
The final line fails because message was created as a local name inside the function.
The name exists only within that local scope during the call.
A useful way to think about the boundary:
caller
│
│ arguments cross in
▼
function local scope
│
│ return value can cross out
▼
caller continues
Local variables do not automatically become available everywhere in the program.
That is a feature, not a limitation. It reduces accidental interference between unrelated parts of the system.
Parameters and return values are deliberate boundary crossings
Instead of relying on hidden global state:
battery = 82
def determine_status():
if battery <= 20:
return "LOW"
prefer a clearer dependency when appropriate:
def determine_status(battery):
if battery <= 20:
return "LOW"
return "NORMAL"
Now the function tells the caller what information it needs.
That makes the behavior easier to test independently:
assert determine_status(20) == "LOW"
assert determine_status(21) == "NORMAL"
You have not reached the formal testing unit yet. Notice how architecture is already making testing easier.
Repetition is a clue, not a command
Repeated code is often a good function candidate:
print("=== REPORT ===")
...
print("=== REPORT ===")
But not every repeated line deserves its own function.
Ask whether the repeated block represents one recognizable job.
Compare:
def show_report_header():
...
with:
def print_equals_signs():
...
The first communicates intent. The second mostly names punctuation.
A function can be too large or too small
Too large:
def run_entire_application():
# input
# validation
# calculations
# files
# menus
# report
# everything
Too fragmented:
def ask_name(): ...
def ask_score(): ...
def print_colon(): ...
def print_newline(): ...
There is no universal number of lines that defines the perfect function.
Look for cohesion: do the statements belong to one responsibility?
Separate logic from input/output when it helps
This function is easy to call but hard to test automatically:
def determine_status():
battery = int(input("Battery: "))
if battery <= 20:
print("LOW")
else:
print("NORMAL")
This version separates concerns:
def determine_status(battery):
if battery <= 20:
return "LOW"
return "NORMAL"
Then another part of the application can handle input and display:
battery = int(input("Battery: "))
status = determine_status(battery)
print(status)
The second design is not automatically mandatory in every tiny script. It becomes valuable when you want to test the logic without pretending to type into the keyboard every time.
Decompose a messy block
Suppose this program:
name = input("Name: ")
battery = int(input("Battery: "))
temperature = float(input("Temperature: "))
if battery <= 10:
status = "CRITICAL"
elif temperature >= 80:
status = "OVERHEATING"
else:
status = "NORMAL"
print("=== DEVICE ===")
print(name)
print(status)
Before writing functions, label responsibilities:
INPUT
name, battery, temperature
LOGIC
turn battery/temperature into status
PRESENTATION
show device report
A possible architecture is:
def determine_status(battery, temperature):
...
def display_report(name, status):
...
Input may remain in the main coordinator for now.
Another design could extract input too. Defend the boundary based on clarity and testability, not because a diagram told you the one official answer.
Scope debugging question
If Python says a name is not defined, ask:
- Where was the name created?
- Is the current line inside that scope?
- Should the value enter through a parameter?
- Should the result leave through
return? - Am I accidentally relying on global state?
That is more useful than making everything global until the error goes away.
Before the refactor in Lesson 4
Look at the Week 5 monitor and mark the responsibilities you see.
For each candidate function, write:
name:
job:
inputs:
returned value:
side effects:
If you cannot describe the job in one clear sentence, the boundary probably needs more thought.
Vocabulary lab
Flip the idea, not just the card
Explain the term before you reveal the back. Then compare your explanation with the definition, example, and warning.
Read all terms without animation
- Scope
- The region of a program where a name is available for use. Example: A local variable created inside a function is available inside that function call. Do not confuse it with: A file path or project directory.
- Decomposition
- Breaking a larger problem into smaller responsibilities that can be understood and implemented separately. Example: Separating status logic from report display. Do not confuse it with: Splitting code into arbitrary small pieces with no meaningful boundary.
- Abstraction
- Exposing a useful interface while hiding implementation details the caller does not need for normal use. Example: Calling determine_status(battery, temperature) without rereading its internal branch logic. Do not confuse it with: Making behavior mysterious or undocumented.
- Function Contract
- The agreed behavior of a function: its purpose, inputs, output, and important side effects or failure expectations. Example: determine_status accepts two readings and returns one status string without printing. Do not confuse it with: The internal line-by-line implementation.
- Side Effect
- An observable effect other than returning a value, such as printing, changing a file, or reading input. Example: print() inside a function creates terminal output. Do not confuse it with: A pure calculation returned to the caller.