Organizing Code with Functions

Parameters and Return Values


Learning Objectives

  • You can write a function that uses return to produce a value.
  • You can use the return value of a function call as part of a larger expression.
  • You can distinguish between a function that prints and a function that returns.
  • You can write a function with multiple parameters.

The functions in the previous chapter were useful but limited. They did things — printed greetings, displayed sessions — but they did not produce values that the rest of the program could use. This chapter introduces return, which turns a function from “a named block of code that does something” into “a named computation that produces a value”.

Producing a Value Using return

A function uses return to give a value back to its caller:

def double(n):
return n * 2
result = double(5)
print(result)

This program prints 10. The function double takes one parameter, multiplies it by two, and returns the result. The call double(5) evaluates to 10, and that 10 is what gets assigned to result.

The right-hand side of the assignment — double(5) — is an expression, just like 5 + 3 or len(subjects). The expression evaluates to a value, and the value can be used anywhere a value is expected:

print(double(5))
print(double(5) + 1)
print(double(double(5)))

This prints 10, 11, and 20. Each of these uses double(5) as part of a larger expression. The double-call in the third line is worth pausing on: the inner double(5) evaluates to 10, which then becomes the argument to the outer call, which returns 20.

Return value expressions

0 / 5 points

Given the following function:

def add_bonus(n):
return n + 2

What does add_bonus(5) * 3 evaluate to?

Printing vs. Returning

The difference between print and return is a common source of confusion. Both are ways for a function to produce something, but they do it in completely different ways:

  • print shows text on the screen.
  • return gives a value back to the caller.

They are completely different operations, and they are not interchangeable.

Compare these two functions:

def print_double(n):
print(n * 2)
def double(n):
return n * 2

They look similar. They are not. Here is what happens when you try to use each one in a larger expression:

print_double(5)
# 10 appears on the screen.
# The call evaluates to None, not 10.
double(5)
# Nothing appears on the screen.
# The call evaluates to 10.

Now try to use them in a calculation:

total = print_double(5) + 1 # TypeError!
total = double(5) + 1 # total is 11.

The first line is a bug. print_double prints something and then returns nothing — technically it returns None, which is a special “no value” value. Trying to add 1 to None fails with a TypeError. The second line works because double actually produces a value.

A function that prints shows you something. A function that returns hands you something. They are different jobs, and most useful functions do exactly one of them. When you write a function, pick one job for it, and stick with it.

A useful rule of thumb: small computation functions (like double, total_minutes, classify_session) should return values, not print them. Functions whose whole point is to interact with the user (like print_summary, greet) can print without returning. Mixing both in the same function — printing a result and returning it — is usually a sign the function is doing too much.

Return instead of print

0 / 5 points

The starter code has a function that prints a doubled value.

Change the function so that it returns the doubled value instead of printing it.

The function must:

  • be named double,
  • take one parameter named n,
  • return n * 2,
  • and not print inside the function.

No user input is needed. Do not print the result. The tests will load your program and call the function directly.

Example calls and return values:

double(5) -> 10
double(-3) -> -6

return Ends the Function Immediately

When return runs, the function ends right there. Whatever value follows the return is sent back to the caller, and any code after the return does not execute:

def classify(minutes):
if minutes < 15:
return "short"
return "long"

If minutes is 10, the if is true, the function returns "short", and the second return is never reached. If minutes is 60, the if is false, the function moves past it, and the second return runs.

This is useful for early exits. You can have several return statements in the same function, and the first one reached is the one that wins:

def classify(minutes):
if minutes < 15:
return "short"
if minutes <= 90:
return "solid"
return "long"

There is no else here, and one is not needed: if the first return ran, the rest of the function did not run, so the second if would not have been reached. Some people prefer the early-return style for this kind of classification. Others prefer the if/elif/else chain because it makes the three branches visually parallel. Both work.

Early return classifier

0 / 5 points

Arrange the lines into a function named pace_label(minutes).

The function should classify a number of minutes and return:

  • "quick" when minutes is less than 20,
  • "steady" when minutes is from 20 through 60,
  • and "deep" when minutes is greater than 60.

Use early returns: once the correct label has been returned, the function should stop.

Functions Without Explicit return

A function that has no return statement does not “return nothing” in the sense of failing to produce a value. It returns a special value called None. The difference matters.

def greet(name):
print(f"Hello, {name}!")
result = greet("Ada")
print(result)

This program prints Hello, Ada! and then None. The first line of output comes from inside the function. The second line comes from printing the return value, which is None because greet had no return.

This is the explanation for a bug you saw in the previous part:

subjects = subjects.append("Math") # bug!

.append() modifies the list in place; it does not return the modified list. Its return value is None. So the line above replaces subjects with None, silently destroying the list. The fix is to call .append() on its own line: subjects.append("Math").

The rule that produces this bug is the rule from this section: a function (or method) with no explicit return value returns None.

Explain an implicit None return

0 / 10 points

Study this code:

def add_topic(topics, topic):
topics.append(topic)
result = add_topic(["math"], "history")
print(result)

Explain why the program prints None. In 3-6 sentences, mention what .append() changes, what the function returns to the caller, and what value is stored in result.

Multiple Parameters

Functions can take more than one parameter. List them in order:

def average(total, count):
return total / count
print(average(120, 4))

This prints 30.0. The call passes 120 to total and 4 to count, in order.

The order of arguments at the call site has to match the order of parameters in the definition. If you reverse them — average(4, 120) — Python will not complain, because both values are numbers, but the result will be 0.0333... rather than 30.0. The function did exactly what it was told. The bug is in the call.

Python also lets you pass arguments by name: average(total=120, count=4). This is called a keyword argument. It is occasionally useful for clarity when calling a function with many parameters. The course will not lean on this much, but you will see it sometimes, especially when using library functions in later parts.

Return average pages

0 / 10 points

Define a function named average_pages(total_pages, days).

The function must:

  • take total_pages and days as parameters,
  • return total_pages / days,
  • not print inside the function,
  • and work when the result is a float.

No user input is needed. Do not print the result. The tests will load your program and call the function directly.

Example calls and return values:

average_pages(120, 4) -> 30.0
average_pages(5, 2) -> 2.5

Building Up: Functions That Take Lists

The patterns from the chapter Counting, Accumulating, and Searching from the last part are exactly the things you want to wrap in functions. For example, calculating a sum of the values in a list could be wrapped in a function as follows:

def total_minutes(minutes_list):
total = 0
for m in minutes_list:
total += m
return total

Calling total_minutes([45, 30, 60]) returns 135. The accumulator is now inside a named function, and the rest of the program can use it like any other expression:

minutes_list = [45, 30, 60]
print(f"Total: {total_minutes(minutes_list)} minutes")

Three lines that, without the function, would have been the entire loop plus the print. The loop has not gone away — it just lives inside total_minutes now, where the main script does not have to look at it.

A second example, this time finding the maximum:

def longest_session(minutes_list):
if len(minutes_list) == 0:
return None
longest = minutes_list[0]
for m in minutes_list:
if m > longest:
longest = m
return longest

Notice the empty-list case is handled explicitly. The function returns None if the list is empty, because there is no “longest of nothing” to report. The caller has to know to handle None, but at least the function does not crash:

result = longest_session(minutes_list)
if result is None:
print("No sessions yet.")
else:
print(f"Longest: {result} minutes")

This is one of several reasonable choices for how to handle the empty case. You could also raise an error, or return 0, or use Python’s built-in max(), which raises an error of its own. Picking which one is right for the function is what writing a specification is about, and that is the topic of Specifying What You Want.

Return total points

0 / 10 points

Define a function named total_points(points).

The function must:

  • take a list of numbers,
  • return the sum of the numbers,
  • return 0 for an empty list,
  • and not print inside the function.

No user input is needed. The tests will load your program and call the function directly.

Example calls and return values:

total_points([3, 5, 2]) -> 10
total_points([]) -> 0

Composing Functions

Once you have several small functions, the interesting move is composing them — using the return value of one as the input to another, or as part of a larger expression.

def total_minutes(minutes_list):
total = 0
for m in minutes_list:
total += m
return total
def average_minutes(minutes_list):
if len(minutes_list) == 0:
return None
return total_minutes(minutes_list) / len(minutes_list)

The second function uses the first. average_minutes does not write its own accumulator loop. It calls total_minutes to get the sum and divides by the length. This is one of the most rewarding things about functions: once you have a small set of building blocks, larger pieces become short.

This is also where the “return vs print” distinction earns its keep. If total_minutes printed the total instead of returning it, average_minutes could not use it — there is nothing to divide. Returning values is what makes functions composable. Printing values does not.

Common Pitfalls

A short list of mistakes that come up often at this stage:

  • return inside a loop, where you meant to keep going. A return ends the function, including any surrounding loop. This is the bug version of the early-exit pattern: if you write return found inside the loop when you meant return found after the loop, the function ends on the first iteration.
  • Calling a printing function and expecting a value. As shown above. If a function prints but does not return, its call evaluates to None.
  • Forgetting to actually call the function. total_minutes (no parentheses) is the function itself, not a number. total_minutes(minutes_list) is the call. If you accidentally print the first, you get something like <function total_minutes at 0x...> instead of a number.
  • Mixing up parameter order. Especially easy when several parameters have the same type. average(120, 4) and average(4, 120) both compile; only one is right.

Check Your Understanding

  1. What is the difference between a function that prints a value and a function that returns a value?
  2. What does print_double(5) + 1 evaluate to, given the definitions earlier in the chapter? Why?
  3. What is the value of f(3) for the following function, and why?
def f(n):
if n > 0:
return "positive"
return "non-positive"
print("unreachable")
  1. Why does subjects = subjects.append("Math") destroy the list?
  2. Rewrite this code as two functions — one that returns the total, one that returns the average — and a main script that uses them:
minutes_list = [45, 30, 60]
total = 0
for m in minutes_list:
total += m
print(total)
print(total / len(minutes_list))

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