Lesson 10 / 18
Function Definition and Call
Parameter and argument, return value, the call stack, the guard clause, and the criteria of a good function.
Contents
The last lesson of the previous topic established that breaking a problem into subproblems is part of solution design, and that functions are its code counterpart. This lesson defines that counterpart.
A function encloses a piece of work in a named, reusable unit — a two-way contract: the caller knows what to provide, the function knows what to return.
Definition and Call
A function definition specifies three things: its name, the values it accepts, and its body.
def average(measurements: list[int]) -> float: """Returns the arithmetic mean of the measurements.""" return sum(measurements) / len(measurements) print(average([12, 18, 7])) # 12.333333333333334 print(average([10, 20])) # 15.0
The measurements in the definition is a parameter: a name in the body that
receives a value at the call. The [12, 18, 7] in the call is an argument: the
value bound to it. The two terms are often confused; the distinction is between what is
expected and what is given.
The definition itself is not executed; it only defines the function. The body runs only when the function is called, so an error inside it may not surface until then.
Return Value
The return statement does two things: it ends the function and produces a value. The
value takes the place of the call; because of this, a call is an expression and can
be placed inside other expressions.
A function can return more than one value, usually by collecting them into a single compound value:
def summarize(measurements: list[int]) -> tuple[float, int]: """Returns the mean and the largest value together.""" largest = measurements[0] for measurement in measurements: if measurement > largest: largest = measurement return average(measurements), largest mean, largest_value = summarize([12, 18, 7, 25, 14]) print(mean, largest_value) # 15.2 25
There are also functions that return no value, called to perform an action such as printing to the screen or saving to a file. This distinction is command–query separation in the software design curriculum: a function changes state or returns a value, not both.
Default and Keyword Arguments
Parameters can be given a default value; if the call does not specify one, the default is used.
def over_limit(measurements: list[int], limit: int = 20) -> list[int]: """Returns the measurements that exceed the limit.""" return [measurement for measurement in measurements if measurement > limit] print(over_limit([12, 18, 7, 25, 14])) # [25] print(over_limit([12, 18, 7, 25, 14], 10)) # [12, 18, 25, 14] print(over_limit([12, 18, 7, 25, 14], limit=15)) # [18, 25]
The syntax in the last line supplies the argument by name. This has two benefits: an ordering mistake becomes impossible, and the value’s meaning is visible in the call — a noticeable gain in readability for calls passing numbers or boolean values.
One warning: a mutable default value leads to surprising behavior in many languages, since the default is created once and shared across calls. Choosing immutable defaults avoids this trap.
The Call Stack
The last lesson of the previous course established that every call pushes a frame onto the stack. This lesson covers the side of that mechanism visible to the programmer.
When a call is made, control passes to the body; the return address, arguments, and
local variables are held in the frame. When return executes, the frame is released
and control resumes at the call site.
When calls nest, frames stack on top of one another. When summarize calls average,
the second frame is pushed atop the first; when average returns, only its own frame
is released.
The practical counterpart of this stack is the trace accompanying error messages: which function called which, from outermost to innermost. This is the first thing to read while debugging; the error’s line sits at the bottom, how execution arrived there above it.
Preconditions and the Guard Clause
The conditions a function’s input must satisfy to work correctly are called
preconditions. The precondition of average is that the list must not be empty;
on an empty list, the division becomes division by zero.
Preconditions are handled at the start of the function. This style is called a guard clause:
def average(measurements: list[int]) -> float: """Returns the mean of the measurements. The list must not be empty.""" if not measurements: # guard clause raise ValueError("measurement list is empty") return sum(measurements) / len(measurements)
The guard clause eliminates the invalid case immediately, leaving the main work
unindented. Wrapping the entire body in an else block gives the same result with
different readability. This is the simplification promised in the conditional
branching lesson.
The second decision is what to do with invalid input: raise an error, return a special value, or produce a default result. The choice depends on the caller’s needs; what matters is documenting the contract and applying it consistently.
Writing the Contract
A function’s contract lives in its signature and its documentation. The description at the start of the body contains everything the caller needs to know:
- What it does — one sentence, with an action verb.
- Parameters — their meaning, units, and valid ranges.
- Return value — what it represents.
- Error conditions — which error is produced under which condition.
- Side effects — if any, stated explicitly.
def over_limit(measurements: list[int], limit: int = 20) -> list[int]: """Returns the measurements that exceed the limit, in a new list. measurements: the measurement values; not modified. limit: the comparison threshold; defaults to 20. returns: the measurements greater than the limit, in their original order. """ return [measurement for measurement in measurements if measurement > limit]
The scope of the documentation is proportional to visibility: a helper used only within its own module can settle for a single line; an interface exposed outward deserves the full contract.
Every assumption left out of the documentation becomes a detail the caller must guess at. Whether the function modifies the list is a typical example, and the subject of the next lesson.
A Good Function
The technical definition of a function is simple; the criteria for a good function, however, have formed through experience:
- It does one thing. If its name is built with “and” (
compute_and_print), it should probably be split in two. - Its name states what it does. A reader of the call should know what will happen without reading the body.
- It is short. The length rule is not strict; the criterion is a body readable on a single screen, in a single breath.
- It takes few parameters. As parameter count grows, the probability of writing the call correctly falls.
- It has no surprises. It produces no side effect that its name does not state.
All of these criteria, with their justifications, are treated in the Software Design and Architectural Principles curriculum. What suffices here is knowing they exist and asking them while writing a function.
Summary
- A function encloses a piece of work in a named, reusable unit; the parameter states the expected name, the argument the given value.
returnends the function and produces a value; a call is an expression usable inside other expressions.- Default and keyword arguments shorten the call and make it readable; mutable default values are a trap.
- Every call pushes a frame onto the stack; the trace in an error message is a view of that stack.
- Preconditions are handled at the start of the function with guard clauses; what to do with invalid input is part of the contract.
Next Step
When a list is given to a function, is a change made inside the function visible to the caller? The answer depends on the language and the type of the value, a direct consequence of the two models introduced in the variables lesson. The next lesson takes up argument passing.
To keep your progress and take notes, Log in
My notes
Log in to take notes.