FREE LESSON · Programming & program design · 1 OF 8
Values, control flow, and function contracts
Values, control flow, and function contracts: Turn a specification into a small program whose behavior you can trace.
Prerequisites: Arithmetic, comparisons, and the idea of an input and output.
Learning objectives
- Trace names, expressions, branches, and loop state.
- Write a function with an explicit input contract.
- Choose edge cases from the contract rather than guessing.
Programs transform a state
A value is data such as the integer 7 or the string "seven". An expression computes a value. A name refers to a value in an environment, and assignment changes that association. In Python, count = count + 1 first reads the old value of count, computes a new integer, and then binds the name to that result. It is an instruction, not an algebraic equality.
A branch chooses which statements execute. A loop repeats a body while advancing through data or while a condition remains true. You can understand both by recording the state after each step. This is more reliable than judging code by how familiar its shape looks.
Consider a function that counts readings meeting a threshold. Its contract accepts a finite iterable of comparable numbers and returns a nonnegative integer. It does not change the readings. The boundary is inclusive: a reading equal to the threshold qualifies.
def count_at_least(readings, threshold):
count = 0
for reading in readings:
if reading >= threshold:
count = count + 1
return count
assert count_at_least([4, 7, 7, 2], 7) == 2
assert count_at_least([], 7) == 0
Trace the first call. Before the loop, count is zero. Reading 4 leaves it zero; the first 7 raises it to one; the second raises it to two; 2 leaves it unchanged. The return sends two back to the caller. Returning inside the loop would end the function early, often after inspecting just one reading. Indentation is therefore part of Python's meaning.
A contract guides the implementation
After each processed prefix, count equals the number of qualifying values in that prefix. This loop invariant explains why initialization, each update, and the final result fit together. A finite input and one advance per iteration establish termination. No intuition about the function's name can replace these obligations.
Separate returning a value from printing it. Printing produces an external effect; returning lets another computation use the result. A calculation function that returns its answer is easier to combine and test. A user interface can decide later how to display it.
The contract also determines tests: empty input, no qualifying readings, all qualifying readings, repeated values, and values exactly on the boundary. Invalid input policy must be explicit. A string mixed with integers may raise an error during comparison; silently inventing a conversion rule would change the function's promised meaning. Begin with a narrow, well-defined function and expand its contract deliberately.
Practice problem
Write longest_run(values, target) to return the longest consecutive run of target in a finite sequence. For [1,1,0,1,1,1] and target 1, the result must be 3; an empty sequence returns 0. State what two pieces of loop state you need and an invariant for each. Trace [0,1,1,0,1] by hand before coding. Include tests for an all-target sequence, a run ending at the final element, and an input with no target. Explain why merely counting all target values is insufficient.
Worked solution
Maintain current, the length of the target run ending at the most recently processed element, and best, the longest run in the processed prefix. Initialize both to zero. For each value, increment current if it equals target, otherwise reset current to zero; then set best to max(best,current). The trace for [0,1,1,0,1] gives current values 0,1,2,0,1 and best values 0,1,2,2,2. Return best after the loop. On [1,1,0,1,1,1], the five matching values are split into runs of two and three, so a total count gives the wrong answer. Both invariants are preserved by each update and imply the result at termination. The final update includes a run that ends at the input boundary.
Check your understanding
Why should count_at_least return after the loop rather than during its first iteration?
- A return ends this function call, so later readings would be skipped.
- Python cannot return integers inside loops.
- A return inside a loop always returns zero.
Explanation
Returning exits the call immediately, regardless of remaining iterations.
Which test distinguishes an inclusive threshold from an exclusive threshold?
- An empty input.
- A reading exactly equal to the threshold.
- A reading far above the threshold.
Explanation
Equality is the boundary on which >= and > disagree.
Primary reading
- Structure and Interpretation of Computer Programs: University of California, Berkeley
- The Python Tutorial: Python Software Foundation