Grade by Grade
A. Programming Concepts and Skills · Arrays and Subprograms

Modular Design: Building Programs from Subprograms

A3.2 A3.1 A4.2About 70 minutes
Learning goals
  • I can break a problem into subtasks (top-down design) and show them in a structure chart.
  • I can write a program as a main() function that calls smaller functions, each with one job.
  • I can pass data between functions with parameters and return values instead of global variables.
  • I can refactor repeated code into a reusable function and test each function on its own.

Words to know

modular design
Building a program from separate, self-contained parts (modules or subprograms), each doing one job.
top-down design (stepwise refinement)
Starting with the whole problem, then breaking it into smaller and smaller subtasks until each is simple enough to code.
structure chart
A diagram of boxes showing the subprograms of a program and which ones call which.
main function
A function, usually called main, that controls the program by calling the other functions in order.
refactor
To improve the structure of code without changing what it does.
reuse
Using a subprogram that already exists, in the same program or another one, instead of writing the code again.
unit test
A check of one small part (usually one function) on its own, e.g. assert count_below([-3, 5], 0) == 1.

Programs too big to hold in your head

A video game, a banking app or the software in a car can have millions of lines of code. Nobody can understand all of that at once. Programmers split big problems into modules: small, separate parts with clear jobs, clear inputs and clear outputs. Each module can be written, tested and fixed on its own, and good modules can be reused in other programs.

You now have every tool for this: functions, parameters, return values and local scope. This lesson shows how to plan a program as a set of subprograms and put them together.

Top-down design

  1. Write the whole task in one sentence: "Report on the maple sap collected at a sugar bush over 5 days."
  2. Break it into big steps. A useful pattern is input → processing → output (from Programming Basics): read the amounts; find the total; find the best day; print a report.
  3. Keep breaking any step that is still complicated. Stop when each step is small enough to be one function of about 5 to 20 lines.
  4. For each function, decide its name, its parameters (what it needs) and its return value (what it gives back).

This is called top-down design or stepwise refinement. A structure chart shows the result.

main()read_amounts(n)total(values)best_day(values)print_report(...)inputprocessingoutputEach box is one subprogram with one job. A line joins a caller to the function it calls.
A structure chart for the sugar bush report. main() is at the top and calls four functions. Each box has one job: one handles input, two do processing, one handles output.

Data flows through parameters and returns

In a modular program, the functions do not share global variables. main() gets data back from one function and passes it as an argument to the next. A good design rule: a function should either compute and return a value or interact with the user (input/print), not both. This keeps the processing functions easy to test and reuse.

main()read_amountsgets n = 5returns the listtotalgets the listreturns 1720.0best_daygets the listreturns 2print_reportgets list, 1720.0, 2returns nothingsolid arrow: call with argumentsdashed arrow: value returned
Data flow for the sugar bush program. main passes n = 5 to read_amounts and gets back the list. It passes the list to total (which returns 1720.0) and to best_day (which returns index 2, shown to the user as day 3), then passes all three to print_report, a procedure that returns nothing.
Worked example

Example 1: The sugar bush report

Write the full program designed above. Use a constant for the sap-to-syrup ratio: roughly 40 L of sap boil down to about 1 L of maple syrup (the exact ratio depends on how sweet the sap is). Test it with the amounts 310, 280, 455, 400 and 275 litres.

  1. Each function in the structure chart becomes one def with a docstring.
  2. best_day returns an index. The report adds 1 to show a day number people expect (day 3, not day 2).
  3. main() is short and reads like the plan: read, compute, compute, report.
  4. The last line, main(), starts the program.

Total sap 1720.0 L, best day 3 (455.0 L), expected syrup 1720 ÷ 40 = 43.0 L.

# Sugar bush report: litres of maple sap collected each day.
SAP_PER_LITRE_SYRUP = 40 # about 40 L of sap make 1 L of syrup
def read_amounts(n):
"""Ask for n daily amounts and return them as a list of floats."""
amounts = []
for day in range(1, n + 1):
amounts.append(float(input("Day " + str(day) + " sap (L): ")))
return amounts
def total(values):
"""Return the sum of a list of numbers."""
result = 0
for v in values:
result += v
return result
def best_day(values):
"""Return the index of the largest value (first one if tied)."""
best = 0
for i in range(1, len(values)):
if values[i] > values[best]:
best = i
return best
def print_report(values, sap_total, best):
"""Print the season summary."""
print()
print("Days recorded:", len(values))
print("Total sap:", sap_total, "L")
print("Best day: day", best + 1, "with", values[best], "L")
print("Expected syrup:", round(sap_total / SAP_PER_LITRE_SYRUP, 1), "L")
def main():
amounts = read_amounts(5)
sap_total = total(amounts)
best = best_day(amounts)
print_report(amounts, sap_total, best)
main()

The complete modular program. Notice that only read_amounts uses input() and only print_report and main deal with output.

Day 1 sap (L): 310
Day 2 sap (L): 280
Day 3 sap (L): 455
Day 4 sap (L): 400
Day 5 sap (L): 275
Days recorded: 5
Total sap: 1720.0 L
Best day: day 3 with 455.0 L
Expected syrup: 43.0 L

A sample run, as it appears on screen. The user typed 310, 280, 455, 400 and 275 after the prompts. The empty line comes from print() with no arguments.

You may see this line

Many Python programs end with if __name__ == "__main__": main() instead of just main(). It means "run main only if this file is run directly". Then another program can import this file to reuse its functions without starting the whole program. This is exactly how the math and random modules work.

Worked example

Example 2: Refactoring repeated code

This program works, but it is one long block with short, unclear names. Refactor it into functions.

temps = [-3, -12, 4, -8, -15, 1, -6]

t = 0

for x in temps:

    t += x

print("Average:", round(t / len(temps), 1))

c = 0

for x in temps:

    if x < 0:

        c += 1

print("Days below zero:", c)

lo = temps[0]

for x in temps:

    if x < lo:

        lo = x

print("Coldest:", lo)

  1. Find the separate jobs: average, count below a limit, lowest value.
  2. Turn each into a function with a meaningful name, parameters and a return value. Replace t, c and lo with names that explain themselves.
  3. Make the limit a parameter (count_below(values, limit)), so the same function can also count days below −10 later. That is generalizing for reuse.
  4. Refactored code: def average(values):     """Return the mean of a non-empty list."""     total = 0     for v in values:         total += v     return total / len(values) def count_below(values, limit):     """Return how many values are less than limit."""     count = 0     for v in values:         if v < limit:             count += 1     return count def lowest(values):     """Return the smallest value in a non-empty list."""     lo = values[0]     for v in values:         if v < lo:             lo = v     return lo def main():     temps = [-3, -12, 4, -8, -15, 1, -6]     print("Average:", round(average(temps), 1))     print("Days below zero:", count_below(temps, 0))     print("Coldest:", lowest(temps)) main()
  5. Run both versions with the same data. Refactoring must not change the output.

Both print Average: -5.6, Days below zero: 5, Coldest: -15.

Testing one module at a time

Because each function depends only on its parameters, you can test it by itself before the rest of the program exists. Python's assert statement checks that something is true and stops with an AssertionError if it is not. A few asserts make a quick set of unit tests.

def count_below(values, limit):
count = 0
for v in values:
if v < limit:
count += 1
return count
assert count_below([-3, 5, -1], 0) == 2
assert count_below([], 0) == 0
assert count_below([0, 0], 0) == 0
print("All tests passed")

Output: All tests passed. The tests include a normal case, an empty list and a boundary case (0 is not below 0). You will build full test plans in Testing and Debugging Programs with Lists and Functions.

Just for fun

A programmer's dream: a function so well designed that it does exactly one thing.

A programmer's reality: a function called do_everything_v2_final_FINAL().

Hands-on activity

Design before you code

You need: paper, a pencil, and a computer with Python.

Pick a real task from home, for example: "Track the minutes our family spends shovelling snow each day for a week, and report the total, the busiest day, and how many days were over 20 minutes."

  1. Draw a structure chart: main at the top, with input, processing and output functions underneath.
  2. Under each box, write the function's header (name and parameters) and what it returns.
  3. Show your chart to a family member and explain it in one minute. Can they understand the plan without seeing code?
  4. Write the functions one at a time, testing each with assert before writing the next. Write main() last.

What to look for: every processing function returns a value and contains no input() or print(); main() is short and reads like the plan.

Your turn

1

What are the three parts of the input-process-output pattern for designing a program?

2

What does a structure chart show?

3

Why is it better for total(values) to return its answer than to print it?

4

In the sugar bush program, which functions could be reused unchanged in a program about daily snowfall?

5

What is wrong with this design? def average(values): print(sum(values) / len(values))

6

Write three assert tests for a function lowest(values).

More practice: warm-up

1

What does "refactor" mean?

2

What is the job of main()?

3

Name the error raised when an assert is false.

4

What naming style should a constant like the sap ratio use?

5

Top-down design is also called stepwise ______.

More practice: on your own

1

Draw (in words) a structure chart for: "Read 10 quiz marks, then print the average, the highest mark and every mark below 50."

2

Write the function marks_below(marks, limit) from the last question so it returns a new list.

3

A student's program has 5 functions, and each one uses the global list data. Explain how to change the design so it is modular.

4

Predict the output:

def a(x):

    return x + 1

def b(x):

    return a(x) * 2

def main():

    print(b(a(3)))

main()

5

Why should the function that reads input be separate from the function that calculates the average?

Challenge

1

Design and write a modular program that reads a sentence and prints the number of words, the longest word, and the sentence with each word capitalized. Show your structure chart in words, then the code.

2

Does best_day in the sugar bush program crash on an empty list? Does print_report? Trace each one.

3

Explain how modular design helps a team of three programmers finish a project faster.

Check your understanding

Quiz

1.

What is top-down design?

2.

In a modular program, how should data usually get from one function to another?

3.

Which function design is the most reusable?

4.

What does assert average([2, 4]) == 3 do if average is correct?

5.

What is a structure chart?

6.

Why does the sugar bush report print best + 1?

For parents and teachers

Big idea: A3.2 includes writing subprograms that perform tasks within programs; modular design is how real programs are built from them. A3.1 appears as reusing existing subprograms (including your own). A4.2 (professional conventions) is practised with docstrings, constants and meaningful names. Strand B (Software Development) will go further with design tools and team projects.

How to help: Ask your teen to explain their structure chart to you before they code. If they cannot explain what a function needs and returns, the design is not ready. The "design before you code" activity is worth doing with a real family task.

Common difficulties:

  • Functions that both calculate and print, so they cannot be reused or tested.
  • Falling back on global variables to share data. Insist on parameters and returns.
  • Forgetting to call main() at the end, so nothing happens when the program runs.
  • Writing the whole program before testing anything. Test each function as soon as it is written.