Learning Goals
3 minBy the end of this lesson you can:
- Write a function that keeps asking until the answer is usable.
- Combine parameters, defaults, returns and locals in one file without globals.
- Test a function on its own before any program depends on it.
- Import your own file into another program and reuse it.
Warm-Up · What Belongs in a Toolbox?
5 minThink about the programs you have written this term. Which of these have you typed more than twice?
- Asking for a number and hoping the user did not type lima.
- Printing a row of dashes.
- Rounding money to two decimal places with
$in front. - Turning a mark out of 100 into a letter.
- Asking a yes/no question.
Two questions. Would I need this in a different program? and can I describe it in one sentence? Two yeses means it belongs in the toolbox. "Print the receipt for Mia's shop" fails the first; "work out the bill, print it, and save it" fails the second and should be three functions.
New Concept · Ask Until It Is Usable
12 minEvery one of these lines is a crash waiting to happen:
age = int(input("Age: ")) # ValueError if they type "two belas"
qty = int(input("How many? ")) # happily accepts -5The fix is a loop that will not let go until the answer is good — and it belongs in a function, because you need it in every program you will ever write.
def ask_number(prompt, low=None, high=None): """Ask until the reply is a whole number inside the allowed range.""" while True: reply = input(prompt).strip() if not reply.lstrip("-").isdigit(): print(" Please type a whole number.") continue value = int(reply) if low is not None and value < low: print(f" Must be at least {low}.") continue if high is not None and value > high: print(f" Must be at most {high}.") continue return value
Four ideas from earlier lessons are all here at once:
- Parameters (FUN-02) so the same function asks any question.
- Defaults (FUN-04) —
low=Nonemeans "no lower limit unless you give one". - Return ends the function (FUN-03), so
return valueis the only way out of the loop. Every bad answer goes round again. - Locals (FUN-05) —
replyandvalueexist only during the call, so two questions in a row cannot interfere.
Save the function, then call it once at the bottom of the file and try to break it on purpose: type a word, type nothing, type -3, type 999. A toolbox function you have not attacked is a bug you have not found yet. Delete the test line once you are satisfied.
Build It · toolbox.py, Four Steps
14 minCreate a file called toolbox.py. Add the functions in order, testing each one before you move on.
Type ask_number from §03, then add its partner:
def ask_yes_no(prompt): """Ask until the reply is a yes or a no. Return True or False.""" while True: reply = input(prompt + " (y/n): ").strip().lower() if reply in ("y", "yes", "ya"): return True if reply in ("n", "no", "no"): return False print(" Please answer y or n.")
Test: call each once and try to break it.
def rm(amount): """Return an amount of money as a tidy string: rm(4.5) -> '$4.50'.""" return f"$ {amount:.2f}" def banner(text, width=34, symbol="*"): """Return a three-line banner with the text centred inside it.""" line = symbol * width middle = symbol + text.center(width - 2) + symbol return line + "\n" + middle + "\n" + line
Both return strings rather than printing them, which is what lets you use them in a receipt, a file, or a message. banner uses defaults, so banner("Menu") is enough.
Test: print(banner("Shop Mia")) and print(rm(4.5)).
def average(numbers): """Return the mean of a list of numbers, or 0 if the list is empty.""" if len(numbers) == 0: return 0 return sum(numbers) / len(numbers) def grade(mark): """Return the letter grade for a mark out of 100.""" if mark < 0 or mark > 100: return "?" if mark >= 80: return "A" if mark >= 65: return "B" if mark >= 50: return "C" return "F"
Both use an early return for the awkward case — an empty list, an impossible mark — so the rest of each function stays simple.
Underneath the six functions, write the program. Every line of it should be short, because all the work is above.
print(banner("MARK BOOK")) marks = [] while True: mark = ask_number("Mark (0-100): ", low=0, high=100) marks.append(mark) print(f" Recorded {mark} - grade {grade(mark)}") if not ask_yes_no("Another"): break print(banner("REPORT", symbol="=")) print(f"Marks entered : {len(marks)}") print(f"Class average : {average(marks):.1f} (grade {grade(average(marks))})") print(f"Best mark : {max(marks)}") print(f"Notional fee : {rm(len(marks) * 2.50)}")
Run it. Type a word when it asks for a mark. Type 150. Type maybe when it asks "Another". None of it should get through, and none of that checking is in the program — it is all in the toolbox.
Make a second file in the same folder called canteen.py, and start it with one line:
from toolbox import ask_number, ask_yes_no, rm, banner
Everything you wrote is now available in the new program without copying a single line. That is what a function was for all along — and it is why the six of them have docstrings, so the next person to open toolbox.py (you, in three months) can see what each one promises.
Try It Yourself
13 minAdd a seventh function: ask_choice(prompt, options), which keeps asking until the reply is one of the strings in options, then returns it. Use it to ask for a drink from a short menu.
Hint
def ask_choice(prompt, options): """Ask until the reply is one of the options. Return it.""" while True: reply = input(f"{prompt} {options}: ").strip().lower() if reply in options: return reply print(" Not on the menu.")
Write a second program, shop.py, that imports your toolbox and takes an order: item name, quantity (1–20), and whether the customer is a student (10% off). Print a banner, the lines, and the total — using rm() for every amount of money.
Rule: shop.py may not contain a single while loop for validation, or a single :.2f. If it does, something belongs in the toolbox instead.
Mini-Challenge 🔥 · Review Someone Else's Toolbox
8 minTheo submitted this as his toolbox. Every function runs. Four of them still fail the two questions from the warm-up. Say what is wrong with each, and how you would fix it.
total = 0
def ask_age():
return int(input("Age: "))
def print_rm(amount):
print("$", round(amount, 2))
def add_to_total(amount):
global total
total = total + amount
def do_everything(marks):
print("=" * 30)
for m in marks:
print(m, "->", "A" if m >= 80 else "B" if m >= 65 else "C")
print("Average:", sum(marks) / len(marks))
print("=" * 30)Answer
ask_age— not reusable (only ever asks one question) and still crashes on a word. Make itask_number(prompt, low, high).print_rm— prints instead of returning, so it can only ever go to the screen. Return the string; let the caller decide what to do with it.add_to_total— aglobal, so it works with exactly one total in exactly one program. Take the total as a parameter and return the new one.do_everything— the name is the review. It prints a border, grades marks, averages them and prints another border: four jobs, and no way to use any one of them alone. Split it intobanner,gradeandaverage, which is exactly what you built in §04.
The fifth problem is above all of them: total = 0 at the top means importing this file into another program silently creates a stray global there too.
Recap · The Whole Section
3 minSix lessons, one idea: name a job once and use the name. def defines and () calls (FUN-01). Parameters let one function serve many cases, matched by position (FUN-02). return hands an answer back so the caller decides what to do with it (FUN-03). Defaults make the common call short and names make any call readable (FUN-04). Locals keep functions independent, which is why data goes in through the brackets and out through return (FUN-05). Put them together and you get a file you keep using (FUN-06).
Vocabulary Card
- validation loop
- A
while Truethat only ends whenreturnhands back a usable answer. - module
- Any
.pyfile you can import from.toolbox.pyis now one. - import
from toolbox import rm, banner— borrow functions from another file in the same folder.- single responsibility
- One function, one job, describable in one sentence. The test
do_everythingfails.
Homework
4 minFinish toolbox.py and hand it in with a second program that imports it.
- At least eight functions, each with a docstring saying what it returns.
- At least two with default values, used both ways somewhere.
- No
global, and noprintinside any function except one you deliberately nameshow_something. - The second program should be under twenty lines, because the toolbox does the work.
Hint: good candidates for the two extra functions — percentage(part, whole), clamp(value, low, high), title_case(name), is_weekend(day).
Sample · the two extra functions, and the program
toolbox.py · added at the end
def percentage(part, whole): """Return part as a percentage of whole, or 0 if whole is 0.""" if whole == 0: return 0 return part / whole * 100 def clamp(value, low, high): """Return value forced into the range low..high.""" return max(low, min(value, high))
tabung.py · a savings tracker, 18 lines
from toolbox import ask_number, ask_yes_no, rm, banner, percentage, clamp print(banner("TABUNG SAYA", symbol="=")) target = ask_number("Savings target ($): ", low=1) saved = 0 while True: saved += ask_number("Amount saved today ($): ", low=0) done = clamp(percentage(saved, target), 0, 100) print(f" {rm(saved)} of {rm(target)} - {done:.0f}% there") if done >= 100: print(banner("TARGET REACHED!")) break if not ask_yes_no("Add another"): break print(f"Final: {rm(saved)}")
Count what is not in the second file: no validation loop, no :.2f, no percentage arithmetic, no border drawing. Eighteen readable lines, because the six-lesson toolbox is doing the rest — which is the entire point of the section.