Building Reusable Functions Overview
1. A long script becomes a set of named tools
Unit 4 gave you the control flow to summarize expedition distances:
That script works once. A function turns the calculation into a tool that other code can call with different routes:
The loop still performs the calculation. The new function boundary answers additional design questions:
- What input must the caller provide?
- What result returns to the caller?
- Which names exist only during one call?
- Does the function change anything outside itself?
- Can another function receive, return, pause, or wrap this behavior?
Those questions—not the number of lines—make functions a design tool.
2. Read every function as a contract
Use this compact contract before examining the body:
| Contract part | Question | total_distance answer |
|---|---|---|
| Purpose | What job has this name? | total route distances |
| Parameters | What must the caller supply? | an iterable of numeric distances |
| Returned result | What crosses back to the caller? | one numeric total |
| Side effects | What observable state changes? | none intended |
| Boundary | What happens for empty input? | returns 0 |
A docstring and annotations may communicate parts of a contract, but the body must actually honor it. Assertions provide executable examples:
A contract is not a promise that every possible value is valid. It identifies the supported inputs and behavior clearly enough that callers can use the function and check important boundaries.
3. Trace the call, not only the final output
When Python evaluates this line:
several moments occur:
| Moment | Active code | Important state |
|---|---|---|
| 1 | caller | builds argument list [4, 7] |
| 2 | function call | binds that list to local parameter distances |
| 3 | function body | creates and updates local total |
| 4 | return |
sends 11 to the suspended caller |
| 5 | caller resumes | formats "Distance: 11 km" and binds label |
Every call receives its own local frame. Later lessons add enclosing scopes, recursive stacks, suspended generator frames, and wrapper calls, but the same questions remain useful: what entered, what executed, what changed, and what returned where?
4. Put behavior in a function for a reason
A function is usually helpful when it provides at least one of these benefits:
- several callers need the same behavior;
- a meaningful name makes the program easier to read;
- one responsibility can be checked independently;
- a changing policy can be supplied as a parameter or callback;
- a complex script becomes a small composition of explicit steps; or
- local state should belong to one call rather than the entire program.
Do not create a function only to make every block shorter. This adds a name without adding meaning:
It may be useful if “advance one game level” is a real reusable rule. It is noise if it merely hides an obvious operator used once. Design from responsibility and contract, not an arbitrary line limit.
5. Your path through this unit
| Lesson | Capability you will build | Main lab artifact |
|---|---|---|
| 1 | Defining Functions and Returning Useful Results — separate definition from execution, return reusable values, and handle every intended path. | Expedition summary |
| 2 | Designing Clear Parameters and Calls — predict argument binding and design safe positional, keyword, default, and variadic interfaces. | Mission-message builder |
| 3 | Breaking a Program into Small Functions — compose cohesive calculations while keeping printing, mutation, and dependencies explicit. | Expedition report pipeline |
| 4 | Following Names Through Function Calls — trace frames, LEGB lookup, return points, closures, and nonlocal state. |
Dispatch-center trace |
| 5 | Passing Functions as Values — configure reusable processing with callbacks, key functions, factories, and small lambdas. | Tournament leaderboard |
| 6 | Solving Smaller Problems with Recursion — define base/progress cases and trace results through a nested data shape. | Nested quest archive |
| 7 | Producing Values One at a Time — explain one-pass consumption and build finite lazy pipelines with yield. |
Sensor stream |
| 8 | Adding Behavior with Decorators — wrap functions while preserving calls, results, metadata, and stacking order. | Achievement tracker |
The Unit Challenge asks you to unlock a nested Moonlit Library with cohesive functions, a recursive generator, callback selection, and an audited unlock operation.
6. Unit 4 supplied the bodies
You already know how to:
- choose complete branches;
- transform, filter, total, count, group, and search in loops;
- explain
whileprogress and stopping; - traverse nested data; and
- build readable comprehensions.
Unit 5 places those algorithms behind explicit call boundaries. The function body is not a new universe: parameters become its starting values, local names hold its working state, and return makes its product available to the caller.
7. Unit 6 will deepen object sharing
This unit identifies whether a function mutates an input and generally builds separate result collections when that keeps the contract simple. Unit 6 develops the full model of identity, aliases, shallow and deep copies, hashability, and mutation across function boundaries.
For now, state the effect plainly:
Neither shape is automatically wrong. The caller must be able to tell which contract applies.
8. Set up a call laboratory
Use separate cells or sections for definitions, source inputs, calls, and checks:
# 1. Definitions
def route_status(stops):
if not stops:
return "no route"
return f"{len(stops)} stops"
# 2. Source inputs
route = ["dock", "ridge", "clinic"]
# 3. Calls and observable results
status = route_status(route)
# 4. Checks
assert status == "3 stops"
assert route == ["dock", "ridge", "clinic"]Restart and run all before trusting stateful examples. A closure can remember old calls, and an iterator can remain partly consumed even when the source code looks unchanged. Clean initialization separates a real rule from leftover notebook state.
9. Choose a realistic pace
Unit 5 is planned for approximately 29–43 hours, including orientation, examples, modifications, checkpoints, eight final labs, and the challenge. The range is guidance, not a deadline.
A sustainable rhythm is:
- state the contract before reading the implementation;
- trace one call through binding, body, and return;
- change one argument or boundary and update the prediction;
- answer a checkpoint without copying its nearby sentence;
- build the final lab before opening support; and
- rerun stateful closure, recursion, iterator, generator, and decorator examples from clean definitions.
10. Check your readiness
Wrap this Unit 4 calculation in a function named usable_total:
Your function must accept a readings collection, return the total of values at least zero, return 0 for empty input, and leave its argument unchanged.
You are ready for Lesson 1 when you can identify the definition, call, argument, parameter, local accumulator, and returned result—and explain why no calculation runs merely because Python reads def.
Key points
- A function gives behavior a name and a call boundary; it is useful when that boundary communicates a real responsibility.
- Read purpose, parameters, returned result, side effects, and boundaries as one contract.
- Trace argument evaluation, binding, local execution, return, and caller resumption separately.
- Unit 4 control flow becomes function-body behavior; Unit 6 will deepen shared object and mutation mechanics.
- Restart stateful examples so old calls or partial consumption cannot imitate correct behavior.