Building from Pieces

Designing Programs from Functions


Learning Objectives

  • You can identify separate responsibilities inside a longer script.
  • You can refactor familiar logic into functions without changing its intended behavior.
  • You can separate user interaction from calculation logic.
  • You can use function names and return values to make the high-level flow of a program visible.
  • You can recognize when creating an extra function would not make a small program clearer.

You can now define functions, pass arguments, and return results. The next question is which parts of a program should become functions. A useful function normally has one understandable responsibility.

Start from the Jobs the Program Performs

Consider this Part 2-style program:

sessions = []
while True:
text = input("Minutes, or done: ").strip().lower()
if text == "done":
break
minutes = int(text)
sessions.append(minutes)
long_sessions = []
for minutes in sessions:
if minutes >= 30:
long_sessions.append(minutes)
total = 0
for minutes in sessions:
total = total + minutes
print(f"Sessions: {len(sessions)}")
print(f"Total minutes: {total}")
print(f"Long sessions: {len(long_sessions)}")

The script has three jobs:

  1. collect sessions;
  2. calculate information about them;
  3. print the summary.

Each job suggests a possible function.

Give a Calculation a Name

Start with a small, self-contained calculation:

def total_minutes(sessions):
total = 0
for minutes in sessions:
total = total + minutes
return total

The caller can now write:

total = total_minutes(sessions)

Although Python has sum(...), this version shows the design principle: the loop becomes a named operation.

The same can be done for the long-session count:

def count_long_sessions(sessions, minimum_minutes):
count = 0
for minutes in sessions:
if minutes >= minimum_minutes:
count = count + 1
return count

The main program no longer needs the calculation details.

Separate Interaction from Calculation

User interaction is often easier to change when it is kept separate from calculation logic.

Compare these two designs.

Mixed together

def show_total():
first = int(input("First: "))
second = int(input("Second: "))
print(first + second)

This function is tied to those prompts and that output format.

Separated

def add(first, second):
return first + second
first = int(input("First: "))
second = int(input("Second: "))
result = add(first, second)
print(result)

The second design makes add reusable:

print(add(2, 3))
print(add(10, -4))

It is also easier to test because calling add does not wait for keyboard input.

Keep calculation functions dependent on parameters and return values when possible. Put input(...) and presentation close to the parts of the program that actually interact with the user.

Functions named read_sessions and print_summary can reasonably interact with the user. Their names should make that responsibility explicit.

A Function for Collecting Data

Part 2 introduced while True and break for open-ended input. That whole interaction can be given a name:

def read_sessions():
sessions = []
while True:
text = input("Minutes, or done: ").strip().lower()
if text == "done":
break
minutes = int(text)
sessions.append(minutes)
return sessions

The main program can now begin with:

sessions = read_sessions()

At this boundary:

  • the caller passes no arguments to read_sessions;
  • the function still obtains information by calling input(...);
  • a list of session lengths comes out.

Calling input(...) is an interaction, not a function argument. The loop is an implementation detail hidden behind read_sessions.

This version still assumes that every entry other than done is a valid integer. Chapter 8 removes that simplifying assumption and shows how the function can ask again after invalid input.

A Function for Presenting a Result

Printing can also be a clear responsibility. In this version, the caller calculates the values and the printing function only presents them:

def print_summary(session_count, total, long_count):
print(f"Sessions: {session_count}")
print(f"Total minutes: {total}")
print(f"Long sessions: {long_count}")

Then the main program makes the calculation and presentation phases visible:

sessions = read_sessions()
total = total_minutes(sessions)
long_count = count_long_sessions(sessions, 30)
print_summary(len(sessions), total, long_count)

The logic now sits behind names describing the program’s phases. Other designs are possible, but here print_summary has one responsibility and the main flow shows where calculations happen.

Write two functions from scratch in natural language, then call them to print the total session length followed by the number of sessions longer than the given minimum.

Write two functions from scratch with natural-language programming

0 / 5 points

This exercise starts with an empty text editor. Write a complete program in natural language in “Natural language program”. Write each step on its own line and indent steps that belong inside a function, loop, or condition. The editor automatically translates your program into Python; inspect “Generated Python” before submitting.

The first function, total_minutes, takes a list of session lengths and returns their sum.

The second function, count_long, takes a list of session lengths and a minimum length, and returns how many sessions are strictly longer than the minimum.

Both functions must return their results without printing inside the functions. They must work for different lists and return 0 for an empty list. count_long must use the minimum supplied by its caller; sessions equal to the minimum do not count.

After both definitions, add calls outside the function bodies. First call total_minutes with [20, 45, 15, 35, 50] and print its result. Then call count_long with the same list and a minimum of 30 and print its result. The complete program must print these two lines in this order, with no additional output:

165
3

Refactoring a Program in Stages

Refactoring means changing the structure of code while preserving its intended, observable behavior.

If the original script works, extract one responsibility, run the program, and then continue.

For the session program above, run a few input sequences before editing and keep the output. After replacing the inline total loop with a call to total_minutes, run the same inputs again:

Responses, one per lineSessionsTotal minutesLong sessions
done000
20, done1200
20, 30, 45, done3952

Check that the three printed summary lines still agree with these results before extracting another piece. If a result changes, inspect the most recent edit first.

A Main Function Can Reveal the Program’s Story

The session summary now shows the work in order: read the sessions, calculate the results, and print them. A reader can follow these calls and inspect a helper’s body when they need the details.

You can give the high-level flow itself a function named main. For the session summary, keep the helper definitions shown earlier in this chapter. Replace the top-level input, calculation, and printing statements with the following definition and call:

def main():
sessions = read_sessions()
total = total_minutes(sessions)
long_count = count_long_sessions(sessions, 30)
print_summary(len(sessions), total, long_count)
main()

main is an ordinary function that coordinates the program. The call at the end starts its work. If you omit the call, none of the statements inside main run.

Do Not Turn Every Few Lines into a Function

Over-decomposition can make a program harder to follow too.

Consider:

def add_one(number):
return number + 1
def show_number(number):
print(number)
def increase_and_show(number):
number = add_one(number)
show_number(number)

These valid functions may add more navigation than clarity in a tiny program.

A useful question is not:

Can these lines technically be moved into a function?

Instead ask:

Does naming this piece make the program easier to understand, test, reuse, or change?

Good programmers may reasonably choose different boundaries.

Function Names Should Describe Responsibilities

Compare a vague name such as process with names such as replace_negative_values_with_zero and average_temperature. process says very little. A precise name tells a reader what role the function plays. The name should also match the exact behavior: a function that replaces values with zero should not be called remove_negative_values.

Names such as these often work well:

  • read_scores;
  • count_vowels;
  • calculate_average;
  • is_valid_guess;
  • select_passing_scores;
  • print_summary.

Function naming is part of design, not decoration.

Review the chapter’s design choices now that all of its guidance is available:

Now refactor a working program. Choose a calculation to give its own helper function, and check that the program still produces the same output after each change.

Festival dashboard

0 / 30 points

The starter is a working festival attendance dashboard. Refactor it into functions while keeping its input and output the same.

Move the calculations into attendance_summary(attendance, minimum). It receives a list of nonnegative integer attendance counts and a nonnegative integer boundary. It returns a new list containing three results in this order:

  • the number of attendance counts;
  • the total number of visitors;
  • the number of counts greater than or equal to minimum.

For example, attendance_summary([120, 85, 150], 100) returns [3, 355, 2]. For an empty list it returns [0, 0, 0]. The function must not print anything or change attendance.

Choose at least one smaller calculation to give its own helper function. Decide its name, parameters, and return value, and use it from attendance_summary. Add a short comment explaining why this is a useful responsibility to separate. Use the loops and accumulators you have practised.

Put the input and printing in a function named main(), then call main() at the end of the program. It reads one line without a visible prompt. The line contains zero or more valid nonnegative integers separated by whitespace. Use attendance_summary with a busy-period boundary of 100, and print its results in the existing format.

For input

120 85 150 90 150

print exactly:

Periods: 5
Total visitors: 595
Busy periods: 3

For an empty input line, all three reported numbers are 0. Run the starter with a few inputs before editing it, then check the same inputs after each change.

Check Your Understanding

  1. What does refactoring mean?
  2. Why are calculation functions often easier to test when they do not call input(...)?
  3. What information comes into and out of read_sessions in the example above?
  4. How can function names make the high-level flow of a program easier to see?
  5. Why is creating the largest possible number of functions not a useful goal?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0