Unit 06 · lesson
Function Contracts Under Pressure
Core path: 30 minutes
A function name can make code look organized while the behavior underneath is still a mess.
The useful question is not "did I make a function?"
It is:
What does this function promise to receive, what does it promise to return, and what side effects does it create?
That promise is the function's contract.
Read a function from the outside first
Consider:
def calculate_damage(base, multiplier):
return base * multiplier
A caller can treat this as:
INPUTS
base: number
multiplier: number
OUTPUT
a number
SIDE EFFECTS
none
The caller does not need to care how multiplication is implemented inside Python. It only needs the contract.
That is abstraction in a very small form.
Follow one call all the way through
Call:
result = calculate_damage(30, 2.5)
Trace it:
caller supplies 30 and 2.5
↓
base = 30
multiplier = 2.5
↓
30 * 2.5
↓
return 75.0
↓
result = 75.0
The argument names in the call do not magically travel into the function. Values are passed into the parameter names defined by the function.
That is why the Week 6 value lab matters: it is showing data movement, not just boxes with arrows.
Use the runnable version as contract evidence:
Run the code to see output.
Change the return expression and observe how the caller receives a value from the function.
Change the arguments without changing the function name. Then change the return expression without changing the call site. Separate what belongs to the caller from what belongs inside the function boundary.
A function that prints is not the same as one that returns
Compare:
def get_status():
print("NORMAL")
with:
def get_status():
return "NORMAL"
The first creates visible output.
The second produces a value another part of the program can use:
status = get_status()
With the printing version, status becomes None unless the function explicitly returns something.
This is a classic beginner trap because the screen can show the correct word while the program state is wrong.
Side effects deserve names too
A function can do more than return values.
These are side effects:
- printing;
- reading keyboard input;
- writing a file;
- changing an object;
- making a network request.
A function with side effects is not automatically bad. You just need to know they exist.
Compare:
def square(number):
return number * number
with:
def ask_for_age():
return int(input("Age: "))
The second depends on external input. That makes it harder to test automatically later.
This is why the monitor refactor separated decision logic from input/output.
Scope: the name may exist only inside the function
def build_message():
message = "READY"
return message
Outside the function:
print(message)
fails because the local name message was created inside the function's scope.
But this works:
status = build_message()
print(status)
The returned value crosses the function boundary. The local variable name does not.
That distinction becomes important when programs grow into multiple files and objects.
Bad decomposition is still bad design
This is technically broken into functions:
def get_name():
...
def print_name():
...
def print_colon():
...
def print_status():
...
More functions did not automatically create better architecture.
A useful boundary usually groups work that belongs together and gives it a name that communicates responsibility.
Do not split code just to increase the function count.
Review your Lesson 4 architecture
Choose three functions from your refactored monitor.
For each one, write:
Function:
Responsibility:
Inputs:
Return value:
Side effects:
Names created locally:
One failure the caller should expect:
Then find one function that is doing too much or has an unclear boundary.
You do not have to rewrite it if the current design is already reasonable. The point is to see the boundary and defend it.
One final test
Take a function that contains logic, such as determine_status().
Run it with at least three direct calls that do not require user input:
assert determine_status(5, 40) == "CRITICAL BATTERY"
assert determine_status(80, 90) == "OVERHEATING"
assert determine_status(80, 40) == "NORMAL"
You have not formally reached the testing unit yet, but notice what decomposition made possible: one small function can now be verified without running the entire application.
That is the real payoff.
The code is not just shorter. The system has boundaries.
Reader workbench
Separate calculation from display
This is the Unit's one-file practice surface. Read the code, predict one result, run it, then change a value, input, condition, or boundary and explain why the evidence changed. Multi-file projects, Git, terminals, packages, and live services still belong in the full development workspace.
One line is returned for each input() call, in order.
Run the code to see output.
This runs in an isolated Python worker in your browser. Your edits stay in this browser until you reset them.