Breaking large problems into smaller ones — helper functions, top-down design, bottom-up testing, single responsibility, and the discipline of refactoring
Programs are not written all at once. They are designed in pieces, built in pieces, tested in pieces, and improved in pieces. Learning to decompose a problem into well-named, focused functions is one of the most important skills a programmer develops.
In Chapters 11, 12, and 13, you learned how to define functions, give them parameters, receive return values, and understand how variables live inside and outside functions. You now know the mechanics of how functions work.
This chapter is about something different: how to think with functions. A function is not only a way to avoid rewriting the same lines. It is a unit of thought. When you give a function a clear name and a single purpose, you are doing more than organizing code. You are saying: this problem has a piece called this, and that piece does exactly that. The program becomes a conversation between named ideas instead of a wall of instructions.
This chapter teaches you how to break a large problem into smaller ones — a practice called decomposition — and how to build programs from focused, reusable, well-named functions. It also teaches you how to spot duplicated code and improve it. By the end of this chapter, you will be able to design a multi-function program before you write it, build and test it piece by piece, and improve messy code into something readable and maintainable.
When you write a function, you are creating a named abstraction. The rest of the program does not need to know how the function works. It only needs to know what it is called and what it does. This is how programmers manage programs that are too large to hold in one thought — and it is the central idea this chapter builds from beginning to end.
By the end of this chapter, you should be able to:
When you first started writing Python programs, a complete program might have been five or ten lines:
name = input("What is your name? ")
print("Hello,", name)
That fits in your head all at once. You can read it, trace it, and understand it in seconds.
Now consider a program that asks a user for their name, age, and city, checks whether their age makes them eligible for a discount, calculates the discounted price based on three different membership tiers, formats a receipt, and saves the result to a file.
That program might be 80 or 100 lines long. If you write it as one continuous block — no functions, no structure, everything in sequence from top to bottom — something uncomfortable happens. You can no longer hold the whole thing in your head at once. When a bug appears on line 67, you have to trace back through every earlier variable to understand what went wrong. When you want to change how discounts are calculated, you have to find every place where discount logic appears and update each one. When you want to test part of the program in isolation, you cannot, because every part depends on every other part.
This is what programmers mean when they say a program has grown in complexity. Complexity is not just length. A 200-line program that is well-organized is often easier to work with than a 50-line program that does everything in one tangled block.
Functions are the main tool for managing complexity. Instead of one long block, you write many small, focused pieces. Each piece has a name. Each piece does one thing. You can read, test, and fix each piece independently.
When you write a function, you are creating a named abstraction. The rest of the program does not need to know how the function works. It only needs to know what it is called and what it does. This is how programmers manage programs that are too large to hold in one thought.
Decomposition means breaking a large problem into smaller subproblems, each of which is easier to solve on its own.
You already do this in everyday life. When someone asks you how to make a meal, you do not describe every muscle movement your hands will make. You describe the steps: gather the ingredients, prepare the vegetables, cook the protein, combine everything, plate and serve. Each step is a named piece of the larger task. You think about each piece at the right level of detail.
Programming decomposition works the same way.
Suppose your task is to write a program that reads student names and scores from the user, calculates the class average, identifies the highest score, and displays a summary. That is one big task. But look at the natural pieces:
Each of those is a smaller, cleaner task. Each one can become a function. When you put them together, the main program reads almost like a plain-English description of what it does:
def main():
students = get_student_data()
average = calculate_average(students)
highest = find_highest(students)
display_summary(students, average, highest)
main()
Notice that main() does not show how any of those things work. It only shows what happens, in order, at the level of named ideas. The details live inside each function. This is decomposition working at its best.
The goal of decomposition is not to make programs longer by adding extra functions for no reason. The goal is to identify the natural structure of the problem and give each piece of that structure its own function.
When you decompose a program, you often find that some functions do part of the work while others coordinate the pieces. The functions that do specific, focused work are called helper functions.
A helper function typically:
Here is a small example. Suppose you are building a program that converts temperatures. You could write it as one block:
choice = input("Convert (C)elsius or (F)ahrenheit? ").upper()
temp = float(input("Enter temperature: "))
if choice == "C":
result = (temp * 9 / 5) + 32
print(f"{temp}°C = {result:.1f}°F")
elif choice == "F":
result = (temp - 32) * 5 / 9
print(f"{temp}°F = {result:.1f}°C")
else:
print("Invalid choice.")
This works for a simple case. But what happens if you want to convert temperatures in three different places in your program? You would have to copy this logic each time. And if the formula ever needed to change, you would have to find every copy.
Now look at the same program decomposed with helper functions:
def celsius_to_fahrenheit(celsius):
return (celsius * 9 / 5) + 32
def fahrenheit_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5 / 9
def get_temperature():
return float(input("Enter temperature: "))
def get_conversion_choice():
return input("Convert (C)elsius or (F)ahrenheit? ").upper()
def display_conversion(original, result, from_unit, to_unit):
print(f"{original}°{from_unit} = {result:.1f}°{to_unit}")
def convert_temperature():
choice = get_conversion_choice()
temp = get_temperature()
if choice == "C":
result = celsius_to_fahrenheit(temp)
display_conversion(temp, result, "C", "F")
elif choice == "F":
result = fahrenheit_to_celsius(temp)
display_conversion(temp, result, "F", "C")
else:
print("Invalid choice.")
convert_temperature()
This version is longer. That might seem like a problem. But read convert_temperature() on its own. It reads almost like a plain description of what happens: get the choice, get the temperature, do the right conversion, display the result. You do not need to understand the math to follow the logic.
More importantly: celsius_to_fahrenheit and fahrenheit_to_celsius are now reusable anywhere in a larger program. If you later build a program that processes weather data from a file, you can call those same functions. Nothing needs to change.
Do one thing. Have a name that says what that one thing is. Take parameters for the information they need. Return values instead of printing (usually). Can be tested independently.
One useful strategy for decomposition is called top-down design. In top-down design, you start from the big picture and break it into pieces before you write any code.
The process works like this:
Here is an example. Suppose you need to write a program that processes a list of exam scores, reports the average, and tells each student whether they passed or failed.
Process exam scores and report results for each student.
Collect student names and scores: ask how many students; for each student, ask for name and score; store them together.
Calculate the average: add up all the scores; divide by the number of students.
Determine pass or fail: compare each student's score to the passing threshold (say, 60); return a label, "Pass" or "Fail".
Display the full report: show the class average; for each student, show name, score, and pass/fail result.
collect_students()
calculate_average(scores)
pass_or_fail(score)
display_report(students, average)
def main():
students = collect_students()
scores = [s["score"] for s in students]
average = calculate_average(scores)
display_report(students, average)
main()
The top-down approach gives you a plan before you write code. This is important because without a plan, it is easy to start writing and then discover that your functions need information they do not have, or that two functions are doing the same work, or that your program structure does not match the problem structure.
Experienced programmers do not always write out the decomposition formally on paper — but they do think through it, often before they type the first line.
After you have your decomposition plan, the most reliable way to build the program is to start from the smallest pieces and test each one before combining them.
This is called bottom-up testing or sometimes incremental development.
Here is why this order matters. If you write all the functions at once and then run the program for the first time, you might get an error. Where is it? Could be anywhere. You have no way to know which piece is working and which is broken. Debugging becomes a search through the entire program.
But if you write and test one function at a time, starting from the smallest helpers, you always know what is working. When a new function fails, you know the problem is in that function — not in something you wrote three days ago.
Write and test the smallest, most independent helpers first. For example, if your program uses a function that checks whether a score is passing:
def pass_or_fail(score):
if score >= 60:
return "Pass"
else:
return "Fail"
Test it immediately, before you write anything else:
print(pass_or_fail(85)) # Should print: Pass
print(pass_or_fail(45)) # Should print: Fail
print(pass_or_fail(60)) # Should print: Pass
These small test calls confirm the function works. Now you can rely on it. When you later call pass_or_fail inside a larger function, you do not need to wonder whether the problem is here — you already know it works.
Then write and test calculate_average:
def calculate_average(scores):
if len(scores) == 0:
return 0
return sum(scores) / len(scores)
print(calculate_average([80, 90, 70])) # Should print: 80.0
print(calculate_average([100, 50])) # Should print: 75.0
print(calculate_average([])) # Should print: 0
Notice the empty list test. When you test functions in isolation, it is natural to think about edge cases: what happens if the list is empty? Testing that now means you will not be surprised by it later.
Write and test each function, working your way up to the full program. When you finally assemble everything in main(), most of the bugs will already be gone.
One of the clearest rules for writing good functions is the single responsibility principle: each function should do one thing.
This sounds simple. In practice, it takes discipline, because it is tempting to combine related things into one function when writing quickly.
Look at this function:
def process_student(name, score):
grade = ""
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"{name}: {score} ({grade})")
total = score + 10 # bonus points
print(f"With bonus: {total}")
This function is doing at least three things: calculating a letter grade, printing results, and applying a bonus. Each of those is a separate responsibility. When you want to change how grades are calculated, you have to edit a function that also handles printing and bonuses. When you want to reuse the grade calculation somewhere else, you cannot — it is stuck inside a function that also prints things.
Compare that to three focused functions:
def letter_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
def apply_bonus(score, bonus=10):
return score + bonus
def display_student_result(name, score):
grade = letter_grade(score)
bonus_score = apply_bonus(score)
print(f"{name}: {score} ({grade}), with bonus: {bonus_score}")
Now each function does exactly one thing. letter_grade can be used anywhere in the program that needs a grade. apply_bonus can be adjusted without touching display logic. display_student_result reads clearly because its sub-steps have names.
If you struggle to write a one-line description of what a function does, and that description includes the word and, the function might be doing too much.
"Calculates the letter grade" — clear, single responsibility.
"Calculates the letter grade and displays the result and applies the bonus" — this function is trying to do too much.
The single responsibility principle does not mean every function must be tiny. Some functions genuinely coordinate several steps, and that coordination is their one responsibility. A main() function that calls several helpers is doing one thing: running the program in order. What the principle prevents is functions that mix concerns — that calculate and display, or validate and format, or compute and save, all in one block.
Function names are not just labels. They are documentation. When you read code you wrote three weeks ago, a well-named function tells you exactly what it does without requiring you to read its body.
# Unclear
def data(score):
...
def result(name, score):
...
# Clear
def calculate_grade(score):
...
def display_student_result(name, score):
...
For functions that return a True/False answer, start the name with is_, has_, or can_:
def is_passing(score):
return score >= 60
def has_bonus_eligibility(score):
return score >= 85
def is_valid_age(age):
return 0 <= age <= 120
These names make conditions read like natural language:
if is_passing(score):
print("Congratulations!")
if has_bonus_eligibility(score):
print("You qualify for a bonus.")
# Too generic
def process(x):
...
def calculate(a, b):
...
# Specific
def process_exam_score(score):
...
def calculate_discounted_price(price, discount_rate):
...
# Unclear abbreviations
def calc_avg_sc(sc_list):
...
# Clear
def calculate_average_score(scores):
...
Keep names honest. A function named get_user_name should return a name. If it also validates the name and saves it to a file, the name is misleading.
Good function names make programs readable without comments. When you find yourself writing a comment to explain what a function does, consider whether a better name would make the comment unnecessary.
Duplicated code is one of the clearest warning signs that a program needs refactoring. When you see the same logic written in two or more places, any future change to that logic must be made in every copy. Forgetting to update one copy creates a bug that is especially hard to find.
Here is a program with duplicated code:
print("--- Morning Report ---")
total = 0
count = 0
for score in morning_scores:
total += score
count += 1
if count > 0:
average = total / count
print(f"Average: {average:.1f}")
else:
print("No scores.")
print("--- Afternoon Report ---")
total = 0
count = 0
for score in afternoon_scores:
total += score
count += 1
if count > 0:
average = total / count
print(f"Average: {average:.1f}")
else:
print("No scores.")
The calculation block is repeated exactly. If you decide to round to two decimal places instead of one, you have to change it in two places. If you have morning, afternoon, and evening reports, you would have three copies.
The fix is to extract the repeated pattern into a function:
def print_score_report(label, scores):
print(f"--- {label} ---")
if len(scores) == 0:
print("No scores.")
return
average = sum(scores) / len(scores)
print(f"Average: {average:.1f}")
print_score_report("Morning Report", morning_scores)
print_score_report("Afternoon Report", afternoon_scores)
Now the logic exists in exactly one place. Change it once and the change applies everywhere the function is called.
If you write the same thing twice, consider a function. If you write it three times, write a function.
Before you start writing code for a program of any real size, it helps to plan the function structure. This does not need to be formal. A few notes or a quick sketch is enough.
Here is a planning process you can use:
Write one or two sentences describing what the program does, not how it works.
Example: The program asks a user to enter a list of grades, then displays the average, highest grade, lowest grade, and how many students passed.
List the things the program must do. Do not write code yet — just identify the tasks.
Each major task is likely a function. Some tasks have sub-steps that become helper functions.
get_grades() → returns a list of grades
calculate_average(grades) → returns the average
find_highest(grades) → returns the highest grade
find_lowest(grades) → returns the lowest grade
count_passing(grades) → returns the number of passing grades
display_report(grades) → prints the full summary
main() → calls everything in order
Before writing the helpers, sketch what main() will look like:
def main():
grades = get_grades()
display_report(grades)
main()
Or, if display_report needs the computed values passed to it:
def main():
grades = get_grades()
average = calculate_average(grades)
highest = find_highest(grades)
lowest = find_lowest(grades)
passing_count = count_passing(grades)
display_report(grades, average, highest, lowest, passing_count)
main()
Start with the simplest helpers. Test each one. Then move to functions that call other functions. Finish with main().
This planning step takes only a few minutes for small programs. For larger programs, it can save hours of frustrating debugging later. A plan does not need to be perfect — it will change as you build — but starting with a structure is far better than writing code without one.
Refactoring means improving the structure of code without changing what it does. A program that was written quickly and works correctly might still benefit from refactoring — especially if it has duplicated code, unclear names, functions that do too much, or logic that would be hard to change later.
Refactoring is not rewriting. You do not throw away working code. You reorganize it, give things better names, and extract repeated patterns into functions. The behavior of the program stays exactly the same.
Here is a before-and-after refactoring example.
name1 = input("Enter student 1 name: ")
score1 = float(input("Enter student 1 score: "))
if score1 >= 60:
label1 = "Pass"
else:
label1 = "Fail"
print(f"{name1}: {score1} — {label1}")
name2 = input("Enter student 2 name: ")
score2 = float(input("Enter student 2 score: "))
if score2 >= 60:
label2 = "Pass"
else:
label2 = "Fail"
print(f"{name2}: {score2} — {label2}")
name3 = input("Enter student 3 name: ")
score3 = float(input("Enter student 3 score: "))
if score3 >= 60:
label3 = "Pass"
else:
label3 = "Fail"
print(f"{name3}: {score3} — {label3}")
This program works, but it has three identical blocks. Adding a fourth student means copying the block again. Changing the passing threshold from 60 to 65 means updating three places.
PASSING_SCORE = 60
def get_student():
name = input("Enter student name: ")
score = float(input("Enter student score: "))
return name, score
def pass_or_fail(score):
if score >= PASSING_SCORE:
return "Pass"
return "Fail"
def display_result(name, score):
label = pass_or_fail(score)
print(f"{name}: {score} — {label}")
def process_students(count):
for _ in range(count):
name, score = get_student()
display_result(name, score)
process_students(3)
The refactored version is shorter, clearer, and easier to change. Adjusting the number of students is one argument. Changing the passing threshold is one variable. Adding a new display format means changing one function.
Refactoring is a habit, not a one-time event. Professional programmers return to old code and improve it. Programs that are never refactored tend to become harder and harder to maintain over time.
This worked example walks through the full process: planning with decomposition, writing helper functions, testing bottom-up, and producing a clean final program.
Write a program that:
collect_students(count) → returns list of (name, score) pairs
calculate_average(scores) → returns average as a float
find_highest(scores) → returns highest score
find_lowest(scores) → returns lowest score
pass_or_fail(score) → returns "Pass" or "Fail"
display_report(students) → prints the full report
main() → coordinates everything
def pass_or_fail(score):
if score >= 60:
return "Pass"
return "Fail"
# Quick tests
print(pass_or_fail(75)) # Pass
print(pass_or_fail(50)) # Fail
print(pass_or_fail(60)) # Pass
def calculate_average(scores):
if len(scores) == 0:
return 0.0
return sum(scores) / len(scores)
# Quick tests
print(calculate_average([80, 90, 100])) # 90.0
print(calculate_average([55, 65])) # 60.0
print(calculate_average([])) # 0.0
def find_highest(scores):
if len(scores) == 0:
return None
return max(scores)
def find_lowest(scores):
if len(scores) == 0:
return None
return min(scores)
# Quick tests
print(find_highest([70, 85, 60, 95])) # 95
print(find_lowest([70, 85, 60, 95])) # 60
def collect_students(count):
students = []
for i in range(count):
print(f"\nStudent {i + 1}:")
name = input(" Name: ")
score = float(input(" Score: "))
students.append((name, score))
return students
def display_report(students):
print("\n--- Class Report ---")
scores = [score for name, score in students]
average = calculate_average(scores)
highest = find_highest(scores)
lowest = find_lowest(scores)
print(f"Number of students: {len(students)}")
print(f"Class average: {average:.1f}")
print(f"Highest score: {highest}")
print(f"Lowest score: {lowest}")
print("\nIndividual Results:")
for name, score in students:
result = pass_or_fail(score)
print(f" {name}: {score:.1f} — {result}")
def main():
print("=== Grade Report Program ===\n")
count = int(input("How many students? "))
students = collect_students(count)
display_report(students)
main()
=== Grade Report Program === How many students? 4 Student 1: Name: Amara Score: 88 Student 2: Name: Ben Score: 54 Student 3: Name: Clara Score: 72 Student 4: Name: Diego Score: 95 --- Class Report --- Number of students: 4 Class average: 77.3 Highest score: 95.0 Lowest score: 54.0 Individual Results: Amara: 88.0 — Pass Ben: 54.0 — Fail Clara: 72.0 — Pass Diego: 95.0 — Pass
Each function has one responsibility. The helper functions (pass_or_fail, calculate_average, find_highest, find_lowest) were tested before they were used inside larger functions. The main program reads like a high-level description of what it does. Changing the passing threshold means changing one line in pass_or_fail. Adding a new field to the report means changing display_report only.
Read each code block carefully. Answer the questions that follow before running the code.
def double(n):
return n * 2
def quadruple(n):
return double(double(n))
result = quadruple(5)
print(result)
double(5) return?double(double(5)) return?quadruple(5) return?def is_even(n):
return n % 2 == 0
def label(n):
if is_even(n):
return "even"
return "odd"
for number in [3, 8, 15, 20]:
print(f"{number} is {label(number)}")
is_even(8) return?label(3) return?def clamp(value, low, high):
if value < low:
return low
if value > high:
return high
return value
print(clamp(5, 0, 10))
print(clamp(-3, 0, 10))
print(clamp(15, 0, 10))
print(clamp(0, 0, 10))
clamp do in your own words?clamp be useful in a real program?Trace through this program step by step. For each function call, record what arguments are passed in and what value is returned.
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def weighted_sum(x, y, weight):
scaled_x = multiply(x, weight)
scaled_y = multiply(y, weight)
return add(scaled_x, scaled_y)
result = weighted_sum(3, 4, 2)
print(result)
Use a tracing table like this:
| Call | Arguments | Return Value |
|---|---|---|
multiply(3, 2) | a=3, b=2 | ? |
multiply(4, 2) | a=4, b=2 | ? |
add(?, ?) | a=?, b=? | ? |
weighted_sum(3, 4, 2) | x=3, y=4, weight=2 | ? |
What is printed?
def greet(name, formal=False):
if formal:
return f"Good day, {name}."
return f"Hey, {name}!"
print(greet("Sam"))
print(greet("Dr. Okonkwo", formal=True))
print(greet("Alex", True))
Trace each call. What is the value of formal in each case? What is printed?
You are given this description of a program:
A quiz program asks the user five questions. For each question, it shows the question text and waits for the user to type an answer. After all five questions, it tells the user how many they got right and shows their score as a percentage.
def line with parameters) for each function you identified.main() would look like, using calls to those functions.Here is a program written as one unstructured block. Your task is not to run it but to identify where functions should be extracted.
print("=== Tip Calculator ===")
bill = float(input("Enter the bill amount: $"))
people = int(input("How many people are splitting? "))
tip_percent = 0.18
tip = bill * tip_percent
total = bill + tip
per_person = total / people
print(f"\nBill: ${bill:.2f}")
print(f"Tip (18%): ${tip:.2f}")
print(f"Total: ${total:.2f}")
print(f"Per person: ${per_person:.2f}")
For each of the following tasks, write a function signature (def line with parameters). Choose a clear name, appropriate parameters, and decide what the function should return.
Explain what is wrong with the design of each function below. Then write an improved version.
def do_stuff(x):
result = x * 2
print(result)
return result * 3
def check(a, b, c, d, e):
return a + b + c + d + e > 100
def greet_and_save_and_validate(name, file):
if name == "":
print("Error")
print(f"Hello, {name}")
file.write(name)
This program has significant code duplication. Refactor it so the repeated logic appears only once.
print("Category 1 Results")
scores1 = [72, 85, 60, 91, 78]
total1 = 0
for s in scores1:
total1 += s
avg1 = total1 / len(scores1)
print(f"Average: {avg1:.1f}")
passing1 = 0
for s in scores1:
if s >= 60:
passing1 += 1
print(f"Passing: {passing1} of {len(scores1)}")
print("Category 2 Results")
scores2 = [55, 90, 48, 67, 83]
total2 = 0
for s in scores2:
total2 += s
avg2 = total2 / len(scores2)
print(f"Average: {avg2:.1f}")
passing2 = 0
for s in scores2:
if s >= 60:
passing2 += 1
print(f"Passing: {passing2} of {len(scores2)}")
After refactoring, your program should produce identical output but with no duplicated blocks of logic.
Examine this function. It has a single responsibility problem. Identify what it is doing and split it into two or three focused functions.
def process_order(item_name, quantity, price_each):
subtotal = quantity * price_each
if quantity >= 10:
discount = subtotal * 0.1
else:
discount = 0
total = subtotal - discount
print(f"Item: {item_name}")
print(f"Quantity: {quantity}")
print(f"Subtotal: ${subtotal:.2f}")
print(f"Discount: ${discount:.2f}")
print(f"Total: ${total:.2f}")
return total
Each of the following programs has one or more errors. Identify the error(s), explain what went wrong, and write a corrected version.
def square(n):
result = n * n
print(square(5))
def greet(name):
print(f"Hello, {name}!")
message = greet("Maria")
print(message.upper())
def calculate_total(prices):
total = 0
for price in prices:
total = price
return total
print(calculate_total([10, 20, 30]))
def is_teenager(age):
return 13 <= age >= 19
print(is_teenager(16))
print(is_teenager(25))
Task 1. Write two functions: feet_to_meters(feet) and meters_to_feet(meters). Each should take one number and return the converted value. Then write a small program that asks the user which conversion they want, takes a value, and displays the result. Use your functions rather than putting the formulas directly in the main code.
Task 2. Write a function is_palindrome(word) that returns True if the word reads the same forwards and backwards, and False otherwise. Then write a program that repeatedly asks the user for a word and says whether it is a palindrome. The program should stop when the user types quit.
Task 3. Write three helper functions: count_vowels(word), count_consonants(word), and word_summary(word). The third function should call the first two and return a formatted string like "hello: 2 vowels, 3 consonants". Write a main program that asks the user for several words and prints a summary for each.
Task 4. Write a function clamp(value, minimum, maximum) that returns minimum if value is below it, maximum if value is above it, and value otherwise. Write a second function normalize(value, low, high) that uses clamp to ensure the value is within range and then scales it to a 0.0–1.0 range. Test both functions with a variety of inputs.
Task 5. Take the following program and improve it using the principles from this chapter. Give functions better names, remove duplicated code, apply single responsibility, and make main() readable as a high-level description of what the program does.
a = input("Enter your name: ")
b = input("Enter your age: ")
c = int(b)
if c >= 18:
d = "adult"
else:
d = "minor"
e = 2024 - c
print(f"Hello {a}. You are an {d}.")
print(f"You were born around {e}.")
a2 = input("Enter your friend's name: ")
b2 = input("Enter your friend's age: ")
c2 = int(b2)
if c2 >= 18:
d2 = "adult"
else:
d2 = "minor"
e2 = 2024 - c2
print(f"Hello {a2}. You are an {d2}.")
print(f"You were born around {e2}.")
Build a menu-driven utility program that offers the user a set of tools, accepts their choice, runs the chosen tool, and returns to the menu until the user decides to quit.
Your program must be designed around functions. The program structure should be visible in main() without needing to read every line of every function.
The menu must offer at least four of the following tools:
main()=== Utility Program === 1. Temperature Converter 2. Tip Calculator 3. Word Analyzer 4. Grade Calculator Q. Quit Enter your choice:
Before writing any code, complete these planning steps:
main() will look like.Submit your planning notes alongside your code.
| Criterion | Description |
|---|---|
| Decomposition | Is each major task in its own function? |
| Single responsibility | Does each function do one thing? |
| Clear naming | Do function names explain their purpose? |
| No duplication | Are repeated patterns extracted into functions? |
| Main readability | Does main() read like a high-level description? |
| Correctness | Does each tool produce accurate results? |
| Robustness | Does the program handle bad input gracefully? |
do_thing(x). What is wrong with this name? Rewrite it as a more appropriate name for a function that checks whether a number is within a valid age range.Chapter 14 taught you to organize programs using functions you write yourself. But Python gives you access to a large collection of functions and tools that are already written, tested, and ready to use — the standard library. Chapter 15 introduces modules: separate files of code that you can import into your programs. You will learn how to import a module, how to use functions from the math and random modules, how dates and times work, how to read simple documentation, and how to write your own module at an introductory level. A key idea in Chapter 15 is that good programmers do not build everything from scratch. They know which tools already exist, how to find them, and how to use them reliably. The standard library is one of Python's greatest strengths, and learning to navigate it will make your programs faster to build and more dependable.