Programming with Python › Functions and Program Design › Day 57
Hands-on lab — Day 57: Functions: Definition, Arguments, and Return Values
- ← Back to the Day 57 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-057-functions-definition-arguments-and-return-values/
Commands
Setup
cd labs/sections/programming-with-python/day-057-functions-definition-arguments-and-return-values
python3 --version Run
python3 examples/demo.py
python3 -c "import sys; sys.path.insert(0, 'examples'); from library import summarize; print(summarize([7, 3, 9, 4, 6]))"
python3 -c "import sys; sys.path.insert(0, 'examples'); from library import greet; print(greet('Ada', punctuation='.'))" Test
bash tests/run_tests.sh File tree
examples/demo.py examples/library.py expected-output/FIELDS.md expected-output/sample-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md security.md starter/design-worksheet.md starter/library.py tests/run_tests.sh troubleshooting.md
Lab README
Day 057 lab — Function Library
Lesson
- Lesson title: Functions: Definition, Arguments, and Return Values
- Day number: 57 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-057-functions-definition-arguments-and-return-values
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-057-functions-definition-arguments-and-return-valueswhen the site is running.
Purpose
Day 57's lesson teaches functions from first principles: def, parameters
versus arguments, positional/keyword/default arguments, return values
(including returning a tuple of several results and the implicit None),
docstrings, the difference between returning a value and printing (pure
function versus side effect), the mutable-default-argument trap, and DRY. This
lab makes that concrete. You build a small library of pure functions —
tiny text and number utilities — each with a docstring, each taking its input
as arguments and handing back a return value, and none of them printing or
changing anything outside themselves. Then you run an automated suite that
calls each function and asserts on what it returns, because return values
are exactly what makes a pure function easy to test. This is the shape every
model call, metric, and data transform takes later: a named, documented,
testable function.
Learning objectives
- Define functions with
def, giving each a docstring whose first line says what the function returns. - Use positional, keyword, and default arguments, and know the difference between a parameter (in the definition) and an argument (at the call).
- Return a value the caller uses — and return a tuple to hand back several results at once, which the caller unpacks.
- Write pure functions (input from arguments, a return value, no side effects) and explain why they are the easy ones to test.
- Avoid the mutable-default-argument trap by defaulting to
Noneand building a fresh container inside. - Factor repeated work into a function (DRY) and prove behaviour by asserting on return values, not by reading printed output.
Prerequisites
- The Day 57 lesson (read it first — it explains every idea this lab builds).
- Days 50–56: conditionals and loops, lists, dictionaries, sets and tuples, comprehensions, and the shape of a script run from the terminal.
- A text editor and a terminal. No experience beyond this course is assumed.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14.0).
- Linux — fully supported (any distribution with Python 3 and bash).
- Windows — use WSL and follow the Linux path, or substitute
pythonforpython3if that is how Python is exposed. The code is pure standard-library Python and behaves identically everywhere.
Hardware requirements
Any computer that runs Python 3. The functions do trivial text and number work; they need no special memory, disk, or GPU.
Required software
python3(3.8 or newer; tested on 3.14.0).bashfor the test runner (preinstalled on macOS and Linux).- Standard library only — no packages to install. See
requirements/README.md.
Free and open-source options
Everything here is free and open source: Python, bash, and the standard library. No account, API key, network access, or purchase is needed. The whole lab is ordinary Python function definitions.
Installation
None beyond Python itself. Move into this directory and you are ready:
cd labs/sections/programming-with-python/day-057-functions-definition-arguments-and-return-values
python3 --version # confirm Python 3.8+ is available
File structure
day-057-functions-definition-arguments-and-return-values/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── library.py ← YOUR working file (6 numbered exercises)
│ └── design-worksheet.md ← design your own library before coding it
├── examples/
│ ├── library.py ← complete reference library of pure functions
│ └── demo.py ← calls the library and uses its return values
├── tests/
│ └── run_tests.sh ← assert-based checks on return values and purity
├── expected-output/
│ ├── sample-run.txt ← real captured run of examples/demo.py
│ ├── test-run.txt ← real captured run of the test suite
│ └── FIELDS.md ← required return values on every platform
├── requirements/
│ └── README.md ← dependency statement (Python 3 only)
├── troubleshooting.md
└── security.md
How to run
From this directory.
## 1. See the finished library in action. demo.py imports it and uses what
## each function returns — no function prints on its own.
python3 examples/demo.py
## 2. Call a function directly and use its return value. summarize returns a
## tuple you can unpack.
python3 -c "import sys; sys.path.insert(0, 'examples'); from library import summarize; c, t, lo, hi, avg = summarize([7, 3, 9, 4, 6]); print('mean is', avg)"
## 3. Try default and keyword arguments on greet and clamp.
python3 -c "import sys; sys.path.insert(0, 'examples'); from library import greet, clamp; print(greet('Ada'), '|', greet('Ada', punctuation='.'), '|', clamp(42, high=100))"
## 4. Your task: complete the six exercises in the starter, then test it.
## (Editing starter/library.py.)
## 5. Check your work — and the reference — with the assert-based suite.
bash tests/run_tests.sh
What the commands do
python3 examples/demo.py— imports the pure functions and prints how a caller uses their return values: composing calls, unpacking the tuple fromsummarize, and passing keyword arguments togreet. The functions themselves never print; the caller does.python3 -c "...summarize..."— imports one function and unpacks its returned tuple(count, total, min, max, mean), showing that one call can hand back several results.python3 -c "...greet, clamp..."— shows default arguments (greet('Ada')usesgreeting="Hello") and keyword arguments (clamp(42, high=100)) at the call site.bash tests/run_tests.sh— imports the reference library and the starter, calls each function, and asserts on the return value: exact results, the returned tuple, default and keyword arguments, purity (same args → same result, inputs unmutated), the mutable-default trap avoided, raised errors, and that every public function has a docstring. Exits 0 only if every check passes.
Expected output
See expected-output/sample-run.txt — a
real captured run of python3 examples/demo.py. Two lines from it:
summarize([7, 3, 9, 4, 6]) ->
count=5 total=29 min=3 max=9 mean=5.8
Every function is pure and deterministic, so your output will match exactly.
expected-output/FIELDS.md lists the required
return value for every function on every platform.
Validation steps
python3 examples/demo.pyprints the block shown above without error.python3 -c "import sys; sys.path.insert(0,'examples'); from library import word_count; print(word_count('the quick brown fox'))"prints4.python3 -c "import sys; sys.path.insert(0,'examples'); from library import summarize; print(summarize([2,4,6]))"prints the tuple(3, 12, 2, 6, 4.0).- Complete the six exercises in
starter/library.py; each function mustreturnits value and carry a docstring. - Run the tests (next section) — every check must pass.
Tests
bash tests/run_tests.sh
Expected final line while the starter is unfinished: 30 checks, 0 failure(s). — the reference library is fully tested and the starter is
checked structurally. Once you complete all six starter exercises, the suite
runs your library through the same assertions, giving 46 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it
can run in CI. A full captured run is in
expected-output/test-run.txt.
Cleanup
The lab writes no data files. Python may create __pycache__ folders when it
imports the module; remove them if you like:
find . -name __pycache__ -type d -prune -exec rm -rf {} +
To reset your work, restore the starter from version control (for example
git restore starter/library.py in the full repository).
Troubleshooting
See troubleshooting.md for the full list: python vs
python3, the NotImplementedError stubs, ModuleNotFoundError and the
import path, "my function prints but the test fails" (return, do not print),
floats versus ints, the mutable-default trap in tally, and unpacking the
tuple from summarize.
Security notes
See security.md. Short version: the code makes no network
calls, needs no privileges, and writes no files. Pure functions have a tiny
contact with the world, which is a security property as well as good design;
never build behaviour out of eval()/exec(), and validate at the boundary
so bad input raises a clear error instead of a silently wrong number.
Extension exercises
- Add a
median(numbers)function that returns the middle value (or the mean of the two middle values) of a sorted copy — without mutating the input, keeping it pure — and add asserts for it to the suite. - Add a
titlecase_words(text)function and refactor any repeated split/join logic shared withreverse_wordsandnormalize_whitespaceinto one small helper (DRY). - Extend
summarizeto also return the range (max - min) as a sixth tuple element, and update every caller and test that unpacks it — noticing how a changed return shape ripples out. - Write your own
tests/test_library.pythat imports the functions and usesassertstatements directly, printingall tests passedonly if every assertion holds, and confirm it exits 0.
Navigation
- Previous day: Day 56 — Building a Data-Driven CLI
(
labs/sections/programming-with-python/day-056-building-a-data-driven-cli/). - Next day: Day 58 — continues Week 9, Functions and Program Design
(
labs/sections/programming-with-python/day-058-.../, to be written). - This week (Week 9): functions and program design — you build the named, documented, testable building blocks that every later data and model tool is assembled from.
Expected output
FIELDS.md
# Expected output — Day 057 lab
These are real captured runs from the authoring machine (macOS, Apple
Silicon, Python 3.14.0, bash 3.2, 2026-07-13). Every function in the library
is **pure and deterministic**: given the same arguments it returns the same
value on every platform Python 3 runs on, so your output will match.
## Files
- `sample-run.txt` — the output of `python3 examples/demo.py`, which imports
the library and *uses* what each function returns: composing calls,
unpacking a returned tuple, and passing keyword arguments.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
still unfinished (30 checks, 0 failures). Absolute paths are shown as
`<repo>`; on your machine they are your real repository path.
## Required behaviour on every platform
A correct library must return exactly these values:
| Call | Returns |
| --- | --- |
| `word_count("the quick brown fox")` | `4` |
| `word_count(" ")` | `0` |
| `normalize_whitespace(" a b ")` | `'a b'` |
| `reverse_words("one two three")` | `'three two one'` |
| `is_palindrome("A man, a plan, a canal: Panama")` | `True` |
| `celsius_to_fahrenheit(100)` | `212.0` |
| `clamp(1.5)` | `1.0` (default range `[0.0, 1.0]`) |
| `clamp(-3, low=-10, high=10)` | `-3` |
| `mean([2, 4, 6])` | `4.0` |
| `summarize([2, 4, 6])` | `(3, 12, 2, 6, 4.0)` (a tuple) |
| `greet("Ada")` | `'Hello, Ada!'` |
| `greet("Grace", greeting="Welcome", punctuation=".")` | `'Welcome, Grace.'` |
| `tally(["a", "b", "a"])` | `{'a': 2, 'b': 1}` |
| `tally(["a"], {"a": 5})` | `{'a': 6}` |
## Required errors
| Call | Result |
| --- | --- |
| `mean([])` | raises `ValueError` (mean of nothing is undefined) |
| `summarize([])` | raises `ValueError` (cannot summarize an empty sequence) |
| `clamp(5, low=10, high=0)` | raises `ValueError` (low greater than high) |
## Purity guarantees the tests check
- **Repeatable:** calling any function twice with the same arguments returns
equal results.
- **No leaked state:** `tally` uses a `None` default (not `{}`), so a second
call never remembers items from a first — the mutable-default trap is
avoided.
- **No mutation of inputs:** `summarize([3, 1, 2])` leaves the list `[3, 1, 2]`
unchanged; `tally(items, start)` does not mutate `start`.
## Platform notes
- The only visible difference between platforms is the shell prompt (`$`)
shown before the demo command; the program's own output is identical.
- `2 + 2 == 4.0` and other float results print the same on CPython 3.8–3.14.
`celsius_to_fahrenheit` returns a float (because of the `/ 5`), so `212.0`,
not `212`, is correct.
- No files are written, no network is used, and no temporary state is left
behind — the tests import the module and assert on return values only.
sample-run.txt
$ python3 examples/demo.py
normalize_whitespace -> 'the quick brown fox'
word_count -> 4
reverse_words -> 'fox brown quick the'
is_palindrome('Racecar') -> True
is_palindrome('function') -> False
greet('Ada') -> 'Hello, Ada!'
greet('Grace', 'Welcome') -> 'Welcome, Grace!'
greet('Alan', punctuation='.') -> 'Hello, Alan.'
clamp(1.5) -> 1.0
clamp(42, high=100) -> 42
summarize([7, 3, 9, 4, 6]) ->
count=5 total=29 min=3 max=9 mean=5.8
mean(readings) -> 5.8
tally(['a','b','a']) -> {'a': 2, 'b': 1}
tally(['x']) -> {'x': 1}
test-run.txt
Testing <repo>/labs/sections/programming-with-python/day-057-functions-definition-arguments-and-return-values/examples/library.py ...
ok: word_count returns 4
ok: word_count of blanks returns 0
ok: normalize_whitespace collapses
ok: reverse_words reverses order
ok: celsius_to_fahrenheit(100)=212
ok: celsius_to_fahrenheit(0)=32
ok: mean([2,4,6]) == 4.0
ok: clamp uses default range [0,1]
ok: clamp with keyword range
ok: greet default greeting
ok: greet keyword arguments
ok: summarize returns a 5-tuple
ok: summarize tuple unpacks
ok: tally counts frequencies
ok: tally with a starting count
ok: tally does not leak across calls
ok: tally does not mutate its input
ok: word_count is pure (repeatable)
ok: summarize does not mutate list
ok: mean([]) raises ValueError
ok: summarize([]) raises ValueError
ok: all public functions documented
Testing examples/demo.py runs ...
ok: demo.py runs and prints results
Testing starter/library.py ...
ok: starter is valid Python
Note: starter/library.py still has unfinished exercises — testing structure only.
ok: starter defines word_count
ok: starter defines reverse_words
ok: starter defines celsius_to_fahrenheit
ok: starter defines clamp
ok: starter defines summarize
ok: starter defines tally
30 checks, 0 failure(s).
Source files
examples/demo.py (2361 bytes)
#!/usr/bin/env python3
"""demo.py — drive the function library and use its return values.
Run this from the lab directory to see the library in action:
python3 examples/demo.py
It imports the pure functions from library.py and *uses what they return* —
composing calls, unpacking a returned tuple, and passing keyword arguments —
without any function printing on its own. All the printing happens here, in
the caller, which is exactly the pure-function-plus-thin-shell shape the
lesson teaches.
"""
import sys
from pathlib import Path
# Make the sibling library.py importable no matter where this is run from.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from library import (
clamp,
greet,
is_palindrome,
mean,
normalize_whitespace,
reverse_words,
summarize,
tally,
word_count,
)
def main():
"""Call the library functions and print how their return values are used."""
sentence = " the quick brown fox "
clean = normalize_whitespace(sentence)
print(f"normalize_whitespace -> {clean!r}")
print(f"word_count -> {word_count(clean)}")
print(f"reverse_words -> {reverse_words(clean)!r}")
print(f"is_palindrome('Racecar') -> {is_palindrome('Racecar')}")
print(f"is_palindrome('function') -> {is_palindrome('function')}")
# Default vs keyword arguments.
print(f"greet('Ada') -> {greet('Ada')!r}")
print(f"greet('Grace', 'Welcome') -> {greet('Grace', 'Welcome')!r}")
print(f"greet('Alan', punctuation='.') -> {greet('Alan', punctuation='.')!r}")
# Default arguments on clamp.
print(f"clamp(1.5) -> {clamp(1.5)}")
print(f"clamp(42, high=100) -> {clamp(42, high=100)}")
# Unpacking a returned tuple of several results.
readings = [7, 3, 9, 4, 6]
count, total, low, high, average = summarize(readings)
print(f"summarize({readings}) ->")
print(f" count={count} total={total} min={low} max={high} mean={average}")
print(f"mean(readings) -> {mean(readings)}")
# Purity of tally: two calls do not leak state into each other.
print(f"tally(['a','b','a']) -> {tally(['a', 'b', 'a'])}")
print(f"tally(['x']) -> {tally(['x'])}")
if __name__ == "__main__":
main()
examples/library.py (5348 bytes)
#!/usr/bin/env python3
"""library.py — a small library of well-documented, pure functions.
Every function here takes its input as arguments and hands back a *return
value*; none of them read the keyboard, print results, or change anything
outside themselves. That makes them "pure": for the same arguments they
give the same answer, and calling them has no side effects. Pure functions
are the easy ones to test, because a test just calls the function and
checks what comes back.
The module is a grab-bag of tiny text and number utilities that together
show every idea from the Day 57 lesson:
- def syntax and a good docstring on every function
- positional, keyword, and default arguments (greet, clamp)
- a return value the caller uses, and the implicit None
- returning a tuple to hand back several results (summarize)
- the mutable-default-argument trap, avoided with None (tally)
Import it and call the functions; nothing runs on import, because there is
no top-level code — only definitions. See examples/demo.py for a session
that calls these functions and uses their return values.
"""
def word_count(text):
"""Return the number of whitespace-separated words in text.
>>> word_count("the quick brown fox")
4
>>> word_count(" ")
0
"""
return len(text.split())
def normalize_whitespace(text):
"""Return text with every run of whitespace collapsed to one space and
the ends stripped.
>>> normalize_whitespace(" hello world ")
'hello world'
"""
return " ".join(text.split())
def reverse_words(text):
"""Return text with its words in reverse order.
>>> reverse_words("one two three")
'three two one'
"""
return " ".join(reversed(text.split()))
def is_palindrome(text):
"""Return True if text reads the same forwards and backwards, ignoring
case and any character that is not a letter or digit.
>>> is_palindrome("A man, a plan, a canal: Panama")
True
>>> is_palindrome("hello")
False
"""
cleaned = [character.lower() for character in text if character.isalnum()]
return cleaned == cleaned[::-1]
def celsius_to_fahrenheit(celsius):
"""Return the Fahrenheit value for a Celsius temperature.
>>> celsius_to_fahrenheit(100)
212.0
>>> celsius_to_fahrenheit(0)
32.0
"""
return celsius * 9 / 5 + 32
def clamp(value, low=0.0, high=1.0):
"""Return value limited to the range [low, high].
low and high are default arguments, so clamp(x) limits x to [0.0, 1.0],
while clamp(x, -10, 10) uses your own range. Passing them by keyword —
clamp(x, high=100) — is clearer at the call site than a bare number.
>>> clamp(1.5)
1.0
>>> clamp(-3, low=-10, high=10)
-3
"""
if low > high:
raise ValueError("low must not be greater than high")
return max(low, min(value, high))
def mean(numbers):
"""Return the arithmetic mean of a sequence of numbers.
Raises ValueError on an empty sequence, because the mean of nothing is
undefined — a clear error is better than a hidden division by zero.
>>> mean([2, 4, 6])
4.0
"""
if not numbers:
raise ValueError("mean of an empty sequence is undefined")
return sum(numbers) / len(numbers)
def summarize(numbers):
"""Return a tuple (count, total, minimum, maximum, mean) for numbers.
This is the multiple-results pattern: instead of five separate function
calls that each re-scan the data, one call hands back a tuple the caller
can unpack:
count, total, low, high, average = summarize(readings)
Raises ValueError on an empty sequence.
>>> summarize([2, 4, 6])
(3, 12, 2, 6, 4.0)
"""
if not numbers:
raise ValueError("cannot summarize an empty sequence")
count = len(numbers)
total = sum(numbers)
return (count, total, min(numbers), max(numbers), total / count)
def greet(name, greeting="Hello", punctuation="!"):
"""Return a greeting line for name.
name is required (positional); greeting and punctuation have defaults.
All three can be passed by position or by keyword, so every call below
is valid:
greet("Ada") -> 'Hello, Ada!'
greet("Ada", "Welcome") -> 'Welcome, Ada!'
greet("Ada", punctuation=".") -> 'Hello, Ada.'
>>> greet("Ada")
'Hello, Ada!'
>>> greet("Grace", greeting="Welcome", punctuation=".")
'Welcome, Grace.'
"""
return f"{greeting}, {name}{punctuation}"
def tally(items, counts=None):
"""Return a frequency dict counting how often each item appears.
The default for counts is None, NOT {}. A mutable default (counts={})
would be created once when the function is defined and then SHARED
across every call, so counts would remember items from previous calls —
the classic mutable-default-argument bug. Using None and building a
fresh dict inside avoids it. When a starting tally is passed in, it is
copied rather than mutated, so this function stays pure.
>>> tally(["a", "b", "a"])
{'a': 2, 'b': 1}
>>> tally(["a"], {"a": 5})
{'a': 6}
"""
result = dict(counts) if counts is not None else {}
for item in items:
result[item] = result.get(item, 0) + 1
return result
metadata.yml (868 bytes)
lesson_id: D057
day: 57
kind: python-program
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-057-functions-definition-arguments-and-return-values
- python3 --version
run_commands:
- python3 examples/demo.py
- python3 -c "import sys; sys.path.insert(0, 'examples'); from library import summarize; print(summarize([7, 3, 9, 4, 6]))"
- python3 -c "import sys; sys.path.insert(0, 'examples'); from library import greet; print(greet('Ada', punctuation='.'))"
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'find . -name __pycache__ -type d -prune -exec rm -rf {} + # optional: remove import caches'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-13'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh -> 30 checks, 0 failure(s), exit 0'
requirements/README.md (947 bytes)
# Dependencies — Day 057 lab
**Python 3 only. No third-party packages.**
- `python3` (3.8 or newer; tested on 3.14.0). Preinstalled on most Linux
distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only Python's built-in language features are used — `def`, arguments,
return values, and the standard modules `sys`, `pathlib`, and `inspect`
that ship with Python. There is deliberately no `requirements.txt`: a
library of pure functions should run on a plain Python install with
nothing to download first.
Check your Python is present and new enough:
```bash
python3 --version
```
If that prints `Python 3.8` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system. The functions are pure standard-library
Python and behave identically everywhere.
starter/design-worksheet.md (1931 bytes)
# Function design worksheet
Fill this in *before* writing code for the practice assignment. Designing a
function on paper first — its name, what it takes, what it returns, and
whether it is pure — is the discipline the lesson teaches. One row per
function you plan to add to your own library.
## The library
- **What family of things does it work with (text? numbers? dates?):**
- **File name (a module of functions, one per idea):**
## Functions
| Function name | Parameters (with defaults) | Returns (type + meaning) | Pure? (no printing / no outside change) | One example call and its result |
| ------------- | -------------------------- | ------------------------ | --------------------------------------- | ------------------------------- |
| | | | | |
| | | | | |
| | | | | |
## Arguments to practise
- **One function that uses a default argument, and why the default is a good
choice:**
- **One function that returns a TUPLE of several results, and what a caller
unpacks from it:**
- **If any function takes a list or dict argument, confirm its default is
`None` (not `[]` or `{}`) and say why:**
## Docstrings
For each function, its docstring's first line should say what it *returns*.
Write that first line here for each:
1.
2.
3.
## Recorded behaviour (fill in after you build it)
- One call and the value it returned:
- One error case (the bad input, and the exception raised):
- Proof it is pure (the same call twice giving the same value, or the input
unchanged after the call):
starter/library.py (3954 bytes)
#!/usr/bin/env python3
"""library.py — YOUR working file: a library of pure functions.
Build this function library one exercise at a time. Each numbered exercise
names exactly what to write, including the docstring it must carry. The
finished reference is in examples/library.py — try each exercise yourself
before peeking.
Rules for every function you write here:
- Give it a docstring: one line saying what it RETURNS, then a short
example. The tests check that each public function has a docstring.
- Make it PURE: take input only from the arguments, return a value, and
do not print or change anything outside the function.
When all six exercises are done, run: bash tests/run_tests.sh
"""
def word_count(text):
"""Return the number of whitespace-separated words in text.
>>> word_count("the quick brown fox")
4
"""
# Exercise 1: RETURN A VALUE.
# Split text on whitespace with text.split() and return how many pieces
# there are (use len). Do not print — return the number.
raise NotImplementedError("Exercise 1: implement word_count")
def normalize_whitespace(text):
"""Return text with runs of whitespace collapsed to one space and the
ends stripped. (Provided as a worked example — read it, then match its
shape in your own functions.)
>>> normalize_whitespace(" hello world ")
'hello world'
"""
return " ".join(text.split())
def reverse_words(text):
"""Return text with its words in reverse order.
>>> reverse_words("one two three")
'three two one'
"""
# Exercise 2: BUILD AND RETURN A STRING.
# Split text into words, reverse the list (reversed(...) or [::-1]), and
# join the words back with a single space. Return the joined string.
raise NotImplementedError("Exercise 2: implement reverse_words")
def celsius_to_fahrenheit(celsius):
"""Return the Fahrenheit value for a Celsius temperature.
>>> celsius_to_fahrenheit(100)
212.0
"""
# Exercise 3: ONE PARAMETER, ONE RETURN.
# Apply the formula celsius * 9 / 5 + 32 and return the result.
raise NotImplementedError("Exercise 3: implement celsius_to_fahrenheit")
def clamp(value, low=0.0, high=1.0):
"""Return value limited to the range [low, high], using DEFAULT arguments
so clamp(x) limits to [0.0, 1.0].
>>> clamp(1.5)
1.0
>>> clamp(-3, low=-10, high=10)
-3
"""
# Exercise 4: DEFAULT ARGUMENTS.
# low and high already have defaults in the signature above — do not
# change them. Return the value pulled back into range:
# max(low, min(value, high))
raise NotImplementedError("Exercise 4: implement clamp")
def summarize(numbers):
"""Return a tuple (count, total, minimum, maximum, mean) for numbers.
>>> summarize([2, 4, 6])
(3, 12, 2, 6, 4.0)
"""
# Exercise 5: RETURN A TUPLE OF SEVERAL RESULTS.
# 1. If numbers is empty, raise ValueError("cannot summarize an empty
# sequence").
# 2. Otherwise compute count (len), total (sum), min, max, and the mean
# (total / count), and return them as a 5-tuple in that order.
raise NotImplementedError("Exercise 5: implement summarize")
def tally(items, counts=None):
"""Return a frequency dict counting how often each item appears.
The default for counts is None, NOT {} — a mutable default would be
shared across calls and remember old items. Build a fresh dict inside.
>>> tally(["a", "b", "a"])
{'a': 2, 'b': 1}
>>> tally(["a"], {"a": 5})
{'a': 6}
"""
# Exercise 6: AVOID THE MUTABLE-DEFAULT TRAP.
# 1. Start result as a COPY of counts when counts is not None
# (result = dict(counts)), otherwise an empty dict {}.
# 2. For each item, do result[item] = result.get(item, 0) + 1.
# 3. Return result. (Keep the None default — do not write counts={}.)
raise NotImplementedError("Exercise 6: implement tally")
tests/run_tests.sh (5729 bytes)
#!/usr/bin/env bash
# Tests for the Day 057 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# The library is a set of PURE functions, so the tests are simple: import a
# function, call it, and assert on the value it returns. Every check drives
# real behaviour — return values, a returned tuple, default and keyword
# arguments, purity (calling twice gives the same answer and does not mutate
# the input), the mutable-default-argument trap, and that every public
# function carries a docstring. It first tests the complete reference
# (examples/library.py), then the learner's starter — structurally while the
# exercises are unfinished, and to the same strict standard once they are
# complete. No network, non-interactive. Exits 0 only if every check passes.
set -u
# Keep the working tree clean: no __pycache__ from the imports below.
export PYTHONDONTWRITEBYTECODE=1
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref_dir="${lab_dir}/examples"
starter_dir="${lab_dir}/starter"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# assert <dir> <label> <python-assertion-body>
# Runs a python snippet with library.py importable from <dir>. A small
# _raises(func, arg) helper is provided for "must raise ValueError" checks.
# The snippet fails the check if it raises anything or exits non-zero.
assert() {
local dir="$1" label="$2" body="$3"
if python3 -c "import sys; sys.path.insert(0, '${dir}')
from library import *
def _raises(func, arg):
try:
func(arg)
return False
except ValueError:
return True
${body}" 2>/dev/null; then
check "${label}" "yes"
else
check "${label}" "no"
fi
}
run_library_checks() {
local dir="$1"
echo "Testing ${dir}/library.py ..."
# --- Return values ---
assert "${dir}" "word_count returns 4" "assert word_count('the quick brown fox') == 4"
assert "${dir}" "word_count of blanks returns 0" "assert word_count(' ') == 0"
assert "${dir}" "normalize_whitespace collapses" "assert normalize_whitespace(' a b ') == 'a b'"
assert "${dir}" "reverse_words reverses order" "assert reverse_words('one two three') == 'three two one'"
assert "${dir}" "celsius_to_fahrenheit(100)=212" "assert celsius_to_fahrenheit(100) == 212.0"
assert "${dir}" "celsius_to_fahrenheit(0)=32" "assert celsius_to_fahrenheit(0) == 32.0"
assert "${dir}" "mean([2,4,6]) == 4.0" "assert mean([2, 4, 6]) == 4.0"
# --- Default and keyword arguments ---
assert "${dir}" "clamp uses default range [0,1]" "assert clamp(1.5) == 1.0"
assert "${dir}" "clamp with keyword range" "assert clamp(-3, low=-10, high=10) == -3"
assert "${dir}" "greet default greeting" "assert greet('Ada') == 'Hello, Ada!'"
assert "${dir}" "greet keyword arguments" "assert greet('Grace', greeting='Welcome', punctuation='.') == 'Welcome, Grace.'"
# --- Returning a tuple of several results ---
assert "${dir}" "summarize returns a 5-tuple" "assert summarize([2, 4, 6]) == (3, 12, 2, 6, 4.0)"
assert "${dir}" "summarize tuple unpacks" "c, t, lo, hi, avg = summarize([7, 3, 9]); assert (c, t, lo, hi) == (3, 19, 3, 9)"
# --- The mutable-default-argument trap is avoided ---
assert "${dir}" "tally counts frequencies" "assert tally(['a', 'b', 'a']) == {'a': 2, 'b': 1}"
assert "${dir}" "tally with a starting count" "assert tally(['a'], {'a': 5}) == {'a': 6}"
assert "${dir}" "tally does not leak across calls" "tally(['x', 'x']); assert tally(['y']) == {'y': 1}"
assert "${dir}" "tally does not mutate its input" "start = {'a': 5}; tally(['a'], start); assert start == {'a': 5}"
# --- Purity: same args -> same result, input unchanged ---
assert "${dir}" "word_count is pure (repeatable)" "assert word_count('a b c') == word_count('a b c') == 3"
assert "${dir}" "summarize does not mutate list" "data = [3, 1, 2]; summarize(data); assert data == [3, 1, 2]"
# --- Errors are raised, not hidden ---
assert "${dir}" "mean([]) raises ValueError" "assert _raises(mean, [])"
assert "${dir}" "summarize([]) raises ValueError" "assert _raises(summarize, [])"
# --- Every public function carries a docstring ---
assert "${dir}" "all public functions documented" "import library, inspect
missing = [n for n, f in inspect.getmembers(library, inspect.isfunction) if not (f.__doc__ or '').strip()]
assert not missing, missing"
}
# --- Reference library: always tested strictly ---
run_library_checks "${ref_dir}"
# --- The demo runs and uses the return values ---
echo "Testing examples/demo.py runs ..."
if python3 "${ref_dir}/demo.py" >/dev/null 2>&1; then
check "demo.py runs and prints results" "yes"
else
check "demo.py runs and prints results" "no"
fi
# --- Learner starter ---
echo "Testing starter/library.py ..."
if python3 -c "compile(open('${starter_dir}/library.py').read(), 'library.py', 'exec')" 2>/dev/null; then
check "starter is valid Python" "yes"
else
check "starter is valid Python" "no"
fi
if grep -q 'NotImplementedError' "${starter_dir}/library.py"; then
echo "Note: starter/library.py still has unfinished exercises — testing structure only."
for fn in word_count reverse_words celsius_to_fahrenheit clamp summarize tally; do
if grep -q "def ${fn}" "${starter_dir}/library.py"; then
check "starter defines ${fn}" "yes"
else
check "starter defines ${fn}" "no"
fi
done
else
run_library_checks "${starter_dir}"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 057 lab
python: command not found
Use python3 explicitly, as every command in this lab does. On macOS and
most Linux systems, bare python may be missing or point to an old version.
Check with python3 --version.
The starter raises NotImplementedError when I import or test it
That is expected until you finish the exercises. Each unfinished function
raises NotImplementedError on purpose so you cannot mistake an empty
function for a working one. Replace each raise NotImplementedError(...)
line with the real body described in the comment above it. Once all six
exercises are done, the file behaves like the reference and the test suite
holds it to the full standard.
ModuleNotFoundError: No module named 'library'
Python looks for modules on its search path (sys.path), which does not
include the examples/ or starter/ subfolder by default. Import checks add
it first, exactly as the tests and demo.py do:
python3 -c "import sys; sys.path.insert(0, 'examples'); import library; print(library.word_count('a b c'))"
Run it from the lab directory (the folder that contains examples/).
My function prints the answer but the test still fails
A test calls your function and checks its return value — it never reads
what you print. A function that prints but does not return hands back
None, so word_count("a b") == 2 is None == 2, which is false. Fix:
return the value; let the caller (like demo.py) do the printing. This
is the whole point of the "return a value, do not print" rule.
celsius_to_fahrenheit(100) gives 212 but the test wants 212.0
Both are equal in Python (212 == 212.0 is True), so the test passes. The
reference returns a float because the formula divides by 5; if you wrote
* 9 // 5 (floor division) you would get a wrong integer. Use * 9 / 5 + 32.
tally remembers items from a previous call
You almost certainly wrote def tally(items, counts={}). That empty dict is
created once, when the function is defined, and is shared by every call,
so it accumulates across calls — the mutable-default-argument trap. Fix: use
def tally(items, counts=None) and build a fresh dict inside the body, as
Exercise 6 describes.
summarize returns five separate things and I cannot capture them
summarize returns one object — a tuple of five values. Capture it whole
(result = summarize(data)) or unpack it in one line:
python3 -c "import sys; sys.path.insert(0,'examples'); from library import summarize; c,t,lo,hi,avg = summarize([2,4,6]); print(c, t, lo, hi, avg)"
The number of names on the left must match the length of the tuple, or Python
raises ValueError: too many values to unpack.
bash: tests/run_tests.sh: Permission denied
Run it through bash explicitly, as the README shows: bash tests/run_tests.sh.
You do not need to chmod +x anything.
Security notes
Security notes — Day 057 lab
-
What the code does: defines a set of pure functions that transform text and numbers and return values. It makes no network connections, needs no privileges, writes no files, and reads no input except the arguments each function is called with.
demo.pyand the test runner only import the module and print or assert on return values. -
Pure functions are safe functions. Because these functions take input only from their arguments and return a value without touching the outside world, they cannot leak data, corrupt a file, or surprise a caller with a side effect. This is not only good design — it is a security property: the smaller a function's contact with the world, the smaller its attack surface and the easier it is to reason about.
-
Never build behaviour out of
eval()orexec(). A tempting shortcut for a "calculator" or "formula" function is to pass a user string toeval(). Do not:evalandexecexecute their argument as Python, so a malicious value could delete files or open a network connection. Every function here does its work with ordinary operations on its arguments — arithmetic, string methods, comprehensions — which can never execute arbitrary code. -
Validate at the boundary; fail loudly.
meanandsummarizeraise a clearValueErroron an empty sequence, andclampraises whenlowexceedshigh, rather than returning a silently wrong number or dividing by zero. An exception a caller can catch is safer than a plausible-looking wrong answer that flows into the next computation unnoticed. -
Reading before running: every file in this lab is short and commented. Read
examples/library.py,examples/demo.py, andtests/run_tests.shbefore running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.