Exercises — Functions — def, parameters, return, docstrings
The mental model we lean on the whole way down:

A function is a box: arguments go in the top slots (parameters), hidden work happens inside, and exactly one value falls out the bottom via return. Every exercise below is really asking "what goes in, what comes out, and what happens inside."
L1 — Recognition
Goal: identify the parts. No running code in your head yet — just name things.
Exercise 1.1
In the code below, name the keyword that starts the definition, the parameter(s), and the docstring.
def triple(x):
"""Return x multiplied by three."""
return x * 3Recall Solution 1.1
- Keyword that starts the definition :::
def - Parameter :::
x(the placeholder name in thedefline) - Docstring :::
"""Return x multiplied by three."""— the first string literal directly underdef.
What we did: matched each word in the box-picture to its job. def opens the box, x is the top slot, the docstring is the sticker label on the side.
Exercise 1.2
For the call triple(4), is 4 a parameter or an argument? What value comes out?
Recall Solution 1.2
4is an ::: argument — the real value that fills the slot namedx.- Value returned :::
12, because4 * 3 = 12.
Slot vs filling: x is the slot (parameter), 4 is what you pour into it (argument).
L2 — Application
Goal: write functions from scratch that actually return the right value.
Exercise 2.1
Write a function rectangle_area(width, height) with a docstring that returns (not prints) the area. Then state what rectangle_area(3, 4) gives.
Recall Solution 2.1
def rectangle_area(width, height):
"""Return the area of a rectangle: width times height."""
return width * heightrectangle_area(3, 4) → ::: 12.
What / Why / What it looks like: we used return (not print) so the caller can do more math with the answer, e.g. total = rectangle_area(3, 4) + 5. In the box picture, 12 falls out the bottom — it does not merely appear on the screen.
Exercise 2.2
Write to_celsius(f) that converts a temperature in Fahrenheit to Celsius using . Give the result for to_celsius(212) and to_celsius(32).
Recall Solution 2.2
def to_celsius(f):
"""Return the Celsius value for a Fahrenheit temperature f."""
return (f - 32) * 5 / 9to_celsius(212):::100.0— boiling water.to_celsius(32):::0.0— freezing water.
Why / 9 and not // 9? We want the exact fractional result, so we use ordinary division / (which returns a float). Integer division // would throw away the fractional part and give wrong answers for temperatures that don't divide evenly.
L3 — Analysis
Goal: run the code in your head. Predict exactly what happens.
Exercise 3.1
What does this program print?
def f():
print("a")
x = f()
print(x)Recall Solution 3.1
It prints two lines:
a
None
::: a comes from the print("a") inside f. Then f has no return, so the call f() evaluates to None; x = None; print(x) shows None.
Why: printing and returning are separate channels. f uses the display channel (print) but leaves the return channel empty → None.
Exercise 3.2
Predict the output:
def g(n):
if n > 0:
return "positive"
print("still running")
return "not positive"
print(g(5))
print(g(-2))Recall Solution 3.2
positive
still running
not positive
::: For g(5): 5 > 0 is true, so return "positive" fires and the function exits immediately — the print("still running") line is never reached. For g(-2): -2 > 0 is false, we skip the first return, run print("still running"), then return "not positive".
What it looks like in the box: return is a trapdoor. Once you fall through it, no code below on that path runs. The -2 path takes a different route through the box, hitting the print on the way.
Exercise 3.3
What does p, q = h(9) bind p and q to?
def h(a):
"""Return the number and its square."""
return a, a * aRecall Solution 3.3
p:::9q:::81
Why: return a, a * a packs the two values into a tuple (9, 81). The line p, q = h(9) then unpacks that tuple — first slot to p, second to q. (See Lists and Tuples for packing/unpacking.)
L4 — Synthesis
Goal: combine parameters, defaults, keywords, and control flow.
Exercise 4.1
Write power(base, exp=2) that returns base ** exp, defaulting to squaring. Give the outputs of power(5), power(2, 3), and power(exp=3, base=2).
Recall Solution 4.1
def power(base, exp=2):
"""Return base raised to exp; squares by default."""
return base ** exppower(5):::25—expomitted, so the default2is used: .power(2, 3):::8—3fillsexpby position: .power(exp=3, base=2):::8— keyword arguments match by name, so order doesn't matter: .
Why defaults exist: they let one definition serve both "the common case" (squaring) and "the general case" without forcing the caller to always spell out exp.
Exercise 4.2
Write clamp(x, low=0, high=10) that returns x forced into the range [low, high]: if x is below low return low, if above high return high, else return x. Give clamp(5), clamp(-3), clamp(99), and clamp(5, low=6).
Recall Solution 4.2
def clamp(x, low=0, high=10):
"""Return x limited to the range [low, high]."""
if x < low:
return low
if x > high:
return high
return xclamp(5):::5— already inside[0, 10].clamp(-3):::0— belowlow, snapped up to0.clamp(99):::10— abovehigh, snapped down to10.clamp(5, low=6):::6— now5 < 6, so it snaps up to the newlow.
Why three returns and not elif? Each early return exits the moment it decides — see Control Flow — if statements. The final return x is only reached when neither bound was violated. We covered all three cases: below, above, and inside.
L5 — Mastery
Goal: the subtle edge cases — mutable defaults, None sentinel, degenerate inputs.
Exercise 5.1 (the classic trap)
Predict the two printed lists:
def add_item(item, bag=[]):
bag.append(item)
return bag
print(add_item("apple"))
print(add_item("banana"))Recall Solution 5.1
['apple']
['banana', 'apple'] # NOT what most people expect
Wait — read carefully. The output is:
['apple']
['apple', 'banana']
::: The default list [] is created once, at definition time, and is shared across every call that omits bag. The first call appends "apple" → ['apple']. The second call reuses the same list and appends "banana" → ['apple', 'banana'].
Why it surprises everyone: [] looks like it means "a fresh empty list each time," but it is evaluated once when def runs, not once per call. The box remembers the same list forever.
Exercise 5.2 (the fix)
Rewrite add_item so each call with no bag starts from a fresh empty list. Give the outputs of two consecutive calls.
Recall Solution 5.2
def add_item(item, bag=None):
"""Append item to bag; start a new list if none is given."""
if bag is None:
bag = []
bag.append(item)
return bag
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana']- First call :::
['apple'] - Second call :::
['banana']
Why the None sentinel works: None is the safe "nothing was passed" flag. The line bag = [] now runs inside the function body, so a brand-new list is built on every call that needs it. Each call gets its own fresh list — no shared memory. (See Variables and Scope: bag is now a fresh local each time.)
Exercise 5.3 (degenerate input)
For def safe_divide(a, b): write a function that returns the quotient a / b, but returns the string "undefined" when b == 0 (division by zero). Give safe_divide(10, 2), safe_divide(7, 0), and safe_divide(0, 5).
Recall Solution 5.3
def safe_divide(a, b):
"""Return a / b, or 'undefined' when b is zero."""
if b == 0:
return "undefined"
return a / bsafe_divide(10, 2):::5.0safe_divide(7, 0):::"undefined"— we guard the zero case before dividing, so Python never raisesZeroDivisionError.safe_divide(0, 5):::0.0— a zero numerator is perfectly fine; . Only a zero denominator is degenerate.
All cases covered: normal divisor, zero divisor (guarded), and zero dividend (allowed). The order matters — check b == 0 first, because touching a / b when b is 0 crashes before any later check can help.
Recall Feynman recap
These ten problems are one idea seen from five heights: a function is a labelled box with input slots and one output chute. L1 names the parts, L2 builds boxes, L3 traces what falls out, L4 wires defaults and branches into the box, L5 handles the boxes that secretly remember (mutable defaults) or break (divide by zero). Guard first, return once, and never confuse showing a number with handing one back.
Connections
- Parent topic — Functions
- Variables and Scope — why the
None-sentinel fix gives each call a fresh local list. - Control Flow — if statements — the branching
returns inclampandsafe_divide. - Lists and Tuples — tuple packing/unpacking in Exercise 3.3.
- Recursion — the next step: a box that calls itself.
- Lambda and Higher-order Functions — one-line anonymous functions.
- DRY Principle — the reason all this exists.