---
title: 'Function Definition and Call'
source: 'https://academia.sh/en/courses/programming-fundamentals/function-definition-and-call'
course: 'Programming Fundamentals'
language: en
updated: '2026-08-17T18:08:24+00:00'
license: 'CC BY-SA 4.0'
---

# Function Definition and Call

Parameter and argument, return value, the call stack, the guard clause, and the criteria of a good function.

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.

```python
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:

```python
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.

```python
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**:

```python
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.

```python
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.
- `return` ends 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.
