6a Return Values and Incremental Development

This lesson introduces the return statement and the rules that come with it, then teaches incremental development: building a function a line at a time, testing against a known answer, and removing the scaffolding at the end.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 6a Return Values and Incremental Development

Title

Python · Chapter 6 — Fruitful functions

§6.1-6.3, pp. 51-54

2. By the end of this lesson you can

Objectives

Five things, each one you can check yourself at an interpreter prompt.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-54 — the pages these objectives are drawn from

3. Before we start: what has been missing?

Warm-up

Every function you have written has the same limitation. Name it.

Discussion prompt

Lesson 3c ended with a function that computed a total into a local variable, and the caller could not get at it. What were your two options for getting information out of a function, and what was wrong with each?

Hint: One of them put characters on a screen. The other did not exist yet.

Answer:

Printing was the only option, and printing sends characters to a display rather than a value to the caller. The program itself cannot use what was printed.

The other option — leaving the value in a local variable — does not work, because local variables are destroyed when the function returns.

So every function so far has been a dead end for its caller. This lesson fixes that: a fruitful function hands a value back, and the caller can assign it, use it in an expression, or pass it on.

4. The one idea behind this lesson: a function can produce a value

Concept

The functions written so far are all void: they have an effect, like printing a value or moving a turtle, but they do not have a return value. In this chapter you will learn to write fruitful functions — and the whole difference is one statement.

return statement — A statement that ends a function immediately and supplies the value the call produces.

Speaking casually, void functions have no return value; more precisely, their return value is None. So they were always returning something — it simply was not anything worth having.

Figure (svg): Two columns contrasting a void function whose call produces None with a fruitful function whose call produces a useful value

The right-hand column is what the return statement buys.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-51

5. The return statement, with an expression

Section

Section 1

6. Sending a value back to the caller

Concept

You have seen the return statement before, in lesson 5c, where it exited a function early. In a fruitful function the return statement includes an expression. It means: return immediately from this function, and use the following expression as a return value.

import math

def area(radius):
    a = math.pi * radius**2
    return a
LineWhat happensEffect
a = math.pi * radius**2compute the area into a local variablea holds the value
return aend the function, hand back athe call produces that value
the callercan assign it, print it, use itthe value survives the frame

The expression can be arbitrarily complicated, so the function could have been written more concisely as a single return statement with the whole formula in it. On the other hand, temporary variables like a can make debugging easier — which is the theme of the second half of this lesson.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-51

7. Picture it: the value crosses back out of the frame

Picture it

Local variables are destroyed, but the returned value is handed over first.

Figure (svg): A call diagram showing main calling area, which computes a value and returns it back to main

This is the answer to lesson 3c's problem. Local names die with the frame; only values can cross between frames, and return is how a value crosses back.

8. Worked example: using a returned value

Worked example

The whole point is what the caller can now do. Try all three uses.

>>> area(3)
28.274333882308138
>>> a = area(3)
>>> a * 2
56.548667764616276
>>> print(area(3) + area(4))
78.53981633974483
UseWhy it worksResult
area(3) at the promptan expression, so its value is displayed28.27...
a = area(3)the value is assigneda holds it
area(3) + area(4)used inside a larger expressionthe two areas are added

Notice that a call is an expression, as it always was.

Why: What has changed is that the expression is now worth something useful rather than None.

Assign it.

Why: Calling the function generates a return value, which we usually assign to a variable or use as part of an expression.

Compose with it.

Why: Two calls inside one arithmetic expression, exactly as lesson 3a's composition rules allow. Nothing new is needed — the rules were always there, and now they apply to your own functions.

Figure (svg): The state of the program after each line of Worked example using a returned value, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

All three work. The call produces a value, and a value can be displayed, assigned, or used inside a larger expression — which is precisely what void functions could not offer.

Verify: Try the same three things with a void function.

Why: print_twice('hi') displays text, assigning it gives None, and adding two calls together raises a TypeError on None. The contrast confirms that it is the return statement, not the call, that makes the difference.

9. Predict: what does this assign?

Prediction

The function prints. Decide what the caller receives.

def area(r):
    print(math.pi * r**2)

a = area(3)
StepWhat happensState
the bodydisplays the area28.27... appears
no return statementthe function is voidreturns None
agets Nonea -> None

Predict first

What does a refer to after this runs?

  • 28.274333882308138
  • None
  • The string of the number
  • Nothing — a is not created

Correct: None — the function printed the value but returned nothing, so the call produced None.

Why: The number appeared on screen, so it certainly was computed. But printing sends characters to a display and returns nothing, so the call produced None and that is what was assigned. This is the single most common bug of this chapter, and the symptom is usually a later TypeError mentioning NoneType, several lines away from the actual mistake.

10. Worked example: return ends the function immediately

Worked example

Not merely sets the answer. Predict what the second print does.

def f(x):
    print('before')
    return x * 2
    print('after')
LineWhat happensNote
print('before')runs normallybefore
return x * 2the function ENDS herethe value is handed back
print('after')never reacheddead code

Run the lines before the return.

Why: Ordinary statements, executed in order.

Reach the return.

Why: As soon as a return statement runs, the function terminates without executing any subsequent statements.

Name what follows it.

Why: Code that appears after a return statement, or any other place the flow of execution can never reach, is called dead code.

Figure (svg): A flow chart showing statements running until a return statement, after which the remaining line is unreachable

It prints before, returns twice x, and never prints after. The line after the return is dead code — legal, unreachable, and never executed.

Verify: Check whether Python warns about it.

Why: It does not. The program runs perfectly and the line is silently ignored, which means dead code is something you have to notice by reading. A print statement that never appears in the output is often the first clue.

11. Trap: printing when you meant to return

Trap

The trap

A student writes a function that computes an area and ends it with print(a) rather than return a.

Treat displaying the answer as producing the answer

Why: On screen the two look identical, especially at the interactive prompt.

The caller receives None. Any attempt to use the result — assigning it, adding to it, passing it on — fails or produces nonsense, and the error message mentions NoneType rather than the missing return.

The fix

Displaying and returning go to different places.

Return the value; let the CALLER decide whether to display it

Why: That is what makes the function reusable: one caller prints it, another adds it to a total.

Treat a TypeError mentioning NoneType as a missing return

Why: It is the standard symptom, and it usually points at a function several lines away rather than at the failing line.

This is lesson 3c's trap arriving with a solution attached. There the answer was you cannot get the value out yet; now there is a way, and forgetting to use it is the commonest bug in this chapter.

12. Discriminate: fruitful or void?

Discrimination

Look for a return statement with an expression.

Sort into buckets

For each function body, is it fruitful or void?

fruitful: produces a value
return x * 2; if x > 0: return x else: return -x; return math.sqrt(x)
void: produces None
print(x * 2); y = x * 2; print(x); return
fruit
Each contains at least one return statement with an expression after it, so the call produces that expression's value.
void
One only prints, one only assigns to a local that is then destroyed, and one has a bare return with no expression — which ends the function and produces None, exactly as falling off the end would.

13. Predict: where does the dead code start?

Prediction

Everything after a reachable return is unreachable.

def f(x):
    if x > 0:
        return 'positive'
    return 'not positive'
    print('done')
LineWhen it is reachedVerdict
line 3returns only when x > 0reachable
line 4reached when the condition was falsereachable
line 5after a return that always runsdead

Predict first

Which line is dead code?

  • Line 3
  • Line 4
  • Line 5
  • None of them

Correct: Line 5 — line 4 returns on every path that reaches it, so nothing after it can ever run.

Why: Line 3 is reachable when the condition is true, and line 4 is reachable when it is false, so both are live. Line 5 sits after a return that is not inside any conditional, which means every path reaching line 4 leaves the function there. Note that a return inside an if is not necessarily the end — only a return that every path must hit makes what follows dead.

14. Think it through: why does return end the function?

Socratic

It could have been designed to merely record the answer. Argue for the actual design.

Discussion prompt

Suppose return only set the value to hand back, and the function continued to the end of its body. Name one thing that would become harder, and one thing that would become possible.

Hint: Think about the absolute_value function with two branches.

Answer:

Harder: early exit. The guardian pattern — check for a bad argument and leave immediately — would need extra machinery, and every function would have to be written so that the rest of the body is safe to run after the answer is known.

Possible: doing cleanup work after deciding the answer. Some languages provide that separately, and Python has one too, in chapter 14.

Ending immediately is the simpler rule and it composes better with conditionals: since these return statements are in an alternative conditional, only one runs — and that sentence is only true because a return leaves at once.

15. Every path must return

Section

Section 2

16. Multiple returns, and the one that is missing

Concept

Sometimes it is useful to have several return statements, one in each branch of a conditional. Since these return statements are in an alternative conditional, only one runs.

def absolute_value(x):
    if x < 0:
        return -x
    else:
        return x
InputWhich branchReturn value
x = -5the first branchreturns 5
x = 5the else branchreturns 5
x = 0the else branchreturns 0

In a fruitful function it is a good idea to ensure that every possible path through the program hits a return statement. If the flow of execution gets to the end of a function without hitting one, the return value is None.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-52

17. Picture it: the path that falls off the end

Picture it

Two conditions, three possible inputs, and one input that reaches neither return.

Figure (svg): A flow chart showing two conditions with returns, and a path for zero that reaches the end of the function without returning

None is not the absolute value of zero. The function is wrong for exactly one input, and it produces no error at all.

18. Worked example: the book's incorrect absolute_value

Worked example

It looks fine and it is wrong for one input. Find which.

def absolute_value(x):
    if x < 0:
        return -x
    if x > 0:
        return x
InputWhich condition holdsReturn value
x = -5first condition truereturns 5
x = 5second condition truereturns 5
x = 0neither condition truereturns None

Test the obvious cases.

Why: A negative and a positive both work. Almost any test you happen to write will pass.

Find the case that neither condition covers.

Why: Zero. If x happens to be 0, neither condition is true, and the function ends without hitting a return statement.

State what comes back.

Why: If the flow of execution gets to the end of a function, the return value is None — which is not the absolute value of 0.

Figure (svg): Two columns comparing the incorrect version using two ifs with the correct version using if and else

The else covers everything the condition excluded, including the boundary.

It returns None for zero. Both conditions are strict, so zero satisfies neither, and the function falls off the end.

Verify: Print the result for zero and compare with abs(0).

Why: print(absolute_value(0)) shows None, and abs(0) is 0. Comparing against the built-in is a clean check, and Python does provide a built-in called abs that computes absolute values — so this function exists to be reasoned about rather than to be used.

19. Predict: what does this return for 5?

Prediction

The condition is true. Trace what happens after the return.

def f(x):
    if x > 0:
        return 'positive'
InputWhat happensReturn value
x = 5the condition is truereturns 'positive'
x = -5the condition is falsefalls off the end
x = -5 resultno return was hitNone

Predict first

What does f(-5) return?

  • 'positive'
  • None
  • False
  • An error

Correct: None — the condition is false, no return statement is reached, and the function ends.

Why: There is no else and no return outside the conditional, so a negative argument reaches the end of the body without returning anything. If the flow of execution gets to the end of a function, the return value is None. Note that f(5) works perfectly, so a test with only positive inputs would never reveal this.

20. Worked example: the compare function

Worked example

The book's exercise: return 1, 0 or -1 depending on how two values compare.

def compare(x, y):
    if x > y:
        return 1
    elif x == y:
        return 0
    else:
        return -1
CaseWhich branchReturn value
x > yfirst branch1
x == ysecond branch0
otherwisethe else-1

Identify the three cases.

Why: Greater, equal, less. Any two numbers fall into exactly one of them, which is the chain shape from lesson 5b.

Give each branch a return.

Why: Because the branches are alternatives, only one return runs — and because the chain ends in an else, one of them always does.

Check exhaustiveness.

Why: The else needs no condition: if x is not greater and not equal, it must be less. Every path hits a return.

Figure (svg): The state of the program after each line of Worked example the compare function, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

Three branches, three return values, and no path that falls off the end — because the chain ends in an else rather than a third condition.

Verify: Test all three cases and confirm none returns None.

Why: compare(3,2) gives 1, compare(2,2) gives 0, compare(1,2) gives -1. Testing every branch is the whole discipline here: a branch you never exercise is a branch that might return None without your knowing.

21. Trap: a chain of ifs instead of a chain with an else

Trap

The trap

A student writes three separate if statements, each returning, covering what they believe are all the cases.

Rely on having thought of every case

Why: The conditions look exhaustive, and for the values tested they were.

Any value the conditions between them miss falls off the end and returns None silently. Boundary values — zero, the empty case, the equal case — are the usual victims.

The fix

End the chain with an else, so exhaustiveness is guaranteed by construction.

Let the last branch be everything remaining

Why: It needs no condition, so it cannot fail to cover a case you did not think of.

Or add an explicit final return outside the conditional

Why: Which is another way of guaranteeing that every path reaches one.

The book's advice is exactly this: in a fruitful function it is a good idea to ensure that every possible path through the program hits a return statement — and an else is the cheapest way to be sure.

22. Error analysis: a function that is wrong for one input

Error analysis

Three inputs, one of which is not handled. Mark the fault and the fix.

Annotate

  • For a positive x the first condition holds and the function returns immediately, so it is correct there.
  • For a negative x the first condition fails, the second holds, and it returns correctly.
  • For zero neither condition holds. Execution reaches the end of the body and the function returns None.
  • That is almost certainly not what was intended: zero has a sign, and the honest answer is a third string.
  • The fix is to make the last case an else rather than a condition, so that it covers everything the first two excluded.
  • The general rule the book gives: in a fruitful function, ensure that every possible path hits a return statement. Two conditions and no else does not guarantee that.

Zero is the boundary of both conditions, and boundary values are exactly where fall-through bugs live.

23. Complete it: make every path return

Faded example

One keyword turns two conditions into an exhaustive pair.

Fill in the blanks

def absolute_value(x):
if x < 0:
return -x
else:
return x

Why: With an else, every input takes exactly one branch and every branch returns, so the function can never fall off the end. Writing a second if with the condition x > 0 would leave zero uncovered, which is the book's own incorrect version. The else is what turns I think I have covered everything into a guarantee.

24. Explain it yourself: why is falling off the end so dangerous?

Explain it to yourself

It produces a value rather than an error. Say why that is worse.

Discussion prompt

A function that falls off the end returns None instead of raising an error. Explain why that makes the bug harder to find than an error would be, and where the eventual failure is likely to appear.

Hint: Think about where None goes next.

Answer:

Because nothing stops. The None is returned, assigned, and carried into the rest of the program, and the failure happens wherever somebody first tries to USE it as a number.

That place can be far from the function with the missing return — a different function, a different file — so the traceback points somewhere unhelpful.

An error at the point of the mistake would have named the function and the line. This is the same asymmetry as lesson 2b's error taxonomy: the failures that announce themselves are the kind ones, and a silently wrong value is the third category.

25. Incremental development: build it a line at a time

Section

Section 3

26. The method for getting a function right

Concept

As you write larger functions, you might find yourself spending more time debugging. Incremental development is a process for avoiding long debugging sessions by adding and testing only a small amount of code at a time.

incremental development — A development plan intended to avoid debugging by adding and testing only a small amount of code at a time.

def distance(x1, y1, x2, y2):
    return 0.0
AspectAssessmentNote
what it computesnothing — always zeroobviously incomplete
what it issyntactically correct and runnabletestable
why that mattersyou can test it before making it complicatedthe whole point

The first step is to consider what the function should look like: what are the inputs, and what is the output? Then write an outline that returns a constant. It obviously does not compute distances, but it is syntactically correct and it runs, which means you can test it before you make it more complicated.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 52-53

27. Picture it: four versions, each one testable

Picture it

Each stage adds a line or two and is checked before the next.

Figure (svg): A four-stage diagram showing the distance function growing from returning zero to computing the differences to squaring them to taking the square root

Every stage produces a program you can run and a number you can check.

The values 3, 4, 25 and 5 are not accidental: the test case was chosen so that every intermediate value is known in advance.

28. Worked example: choosing a test case with a known answer

Worked example

The book picks its numbers deliberately. Work out why these ones.

>>> distance(1, 2, 4, 6)
0.0
Intermediate valueCalculationKnown answer
x2 - x14 - 13
y2 - y16 - 24
the distancethe hypotenuse of a 3-4-5 triangle5

Notice what the numbers were chosen for.

Why: The horizontal distance is 3 and the vertical distance is 4, so the result is 5 — the hypotenuse of a 3-4-5 right triangle.

Notice that the intermediates are known too.

Why: Not just the answer: dx should be 3, dy should be 4, and the sum of squares should be 25. Every stage has a value you can check.

State the principle.

Why: When testing a function, it is useful to know the right answer — and it is even more useful to know the right intermediate answers.

Figure (svg): The state of the program after each line of Worked example choosing a test case with a known answer, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

The test case is chosen so that the answer and every intermediate value are known in advance. That is what makes each stage checkable rather than merely runnable.

Verify: Ask what a badly chosen test case would cost.

Why: With arbitrary coordinates you would get a number like 7.2801... and have no idea whether it was right. Choosing values that make the arithmetic checkable by hand is a real skill, and it is why the book chose a Pythagorean triple.

29. Rank: the stages of incremental development

Ranking

Each stage is runnable, and each is checked before the next.

Put in order

  1. write the outline that returns 0.0; check it runs
  2. compute dx and dy and print them; check they are 3 and 4
  3. compute dsquared and print it; check it is 25
  4. take the square root and return it; check it is 5.0

Why: The outline comes first because it establishes that the function exists, takes the right parameters and can be called — none of which has anything to do with the mathematics. Then each computation is added in the order the formula needs it, with its value checked before the next is built on top. Reordering any two would mean building on something unverified, which is exactly what the method exists to avoid.

30. Worked example: the four stages

Worked example

Each version is run and checked before the next line is added.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = math.sqrt(dsquared)
    return result
StageWhat was addedWhat to check
stage 1return 0.0confirms it runs
stage 2compute dx and dy, print themshould show 3 and 4
stage 3compute dsquared, print itshould show 25
stage 4take the square root and return itshould return 5.0

Add one or two lines at a time.

Why: When you start out you should add only a line or two at a time. As you gain experience you might write and debug bigger chunks.

Print the intermediate values.

Why: If the function is working it should display dx is 3 and dy is 4. If so, we know the function is getting the right arguments and performing the first computation correctly.

Note the payoff when something is wrong.

Why: If not, there are only a few lines to check. That is the entire benefit: a bug introduced by the last two lines can only be in the last two lines.

Figure (svg): A ladder showing the distance calculation for the test case reducing from the coordinates to three and four to twenty-five to five

Every rung is a number you can check by hand. That is what makes the test case a good one.

Four versions, each run and checked. The final one returns 5.0 for the test case, and every intermediate value was verified on the way.

Verify: Compare with writing the whole function at once and finding it returns the wrong number.

Why: With the whole function written, a wrong answer could come from any of five lines and you would have to add print statements to find out which — which is exactly the work incremental development did as it went. The method does not add work; it moves it earlier, where it is cheaper.

31. Trap: writing the whole function before running it

Trap

The trap

A student writes all six lines of distance, runs it, and gets a number that is not 5.

Treat running the code as the last step

Why: It feels efficient: why run something you know is incomplete?

Now any of six lines could be at fault, and the only way to narrow it down is to add the print statements that incremental development would have had all along.

The fix

Start with a working program and make small incremental changes.

Begin with a version that returns a constant

Why: It computes nothing and it proves the name, the parameters and the call all work.

Add a line or two, print the new value, and check it against a known answer

Why: At any point, if there is an error, you should have a good idea where it is.

The claim is not that this is more careful. It is that it is faster, because the alternative is a debugging session that ends up doing the same checks in a worse order.

32. Predict: what should stage two print?

Prediction

The test case was chosen so you know.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    print('dx is', dx)
    print('dy is', dy)
    return 0.0
ValueCalculationExpected
dx4 - 13
dy6 - 24
the returnstill the placeholder0.0

Predict first

For distance(1, 2, 4, 6), what should this stage display?

  • dx is 3, dy is 4 — the two differences
  • dx is 1, dy is 2 — the first point repeated
  • dx is 5, dy is 5 — the distance twice
  • nothing at all, because the function still returns 0.0

Correct: dx is 3, dy is 4 — the two differences of the chosen test case.

Why: The point of this stage is that you know the answer before you run it. If it displays 3 and 4, the function is getting the right arguments and performing the first computation correctly; if not, there are only a few lines to check. Note that the return is still the placeholder — the value being returned is irrelevant at this stage and will be replaced later.

33. Match each key aspect to what it buys

Matching

The book names three. Each addresses a different failure.

Match the pairs

  • a. start with a working program and make small changes
  • b. use variables to hold intermediate values
  • c. remove scaffolding once it works
  • r1. if there is an error, you know roughly where it is
  • r2. you can display and check them as you go
  • r3. the finished function does its job without noise

Why: The three work together: small changes localise a bug, intermediate variables make each step observable, and removing the scaffolding at the end leaves a clean function. The second is worth noticing as a design choice — the book earlier said the whole formula could be one expression, and this is the reason to write it as several lines instead.

34. Step zero: writing a function you do not know how to write

Step zero

Before any of the logic, there is a first line.

Discussion prompt

You are asked to write a function that returns the number of vowels in a word, and you have no idea how. Using incremental development, what is the very first thing you write, and what do you check?

Hint: It should not compute anything.

Answer:

Write the outline: a def line with the parameter, and a body that returns a constant — say 0. Nothing about vowels appears yet.

Then call it and check it runs and returns 0. That confirms the name is right, the parameter is right, and the call site works — three things that have nothing to do with counting vowels.

It feels like a wasted step and it is the opposite: it separates the problem you know how to solve (making a function exist) from the one you do not (counting vowels), so that when the second goes wrong the first is not in question.

35. Scaffolding: the print statements you take down afterwards

Section

Section 4

36. Code that helps you build and is not part of the building

Concept

The final version of distance does not display anything when it runs; it only returns a value. The print statements used during development are helpful for building the program but are not part of the final product.

scaffolding — Code used during program development but not part of the final version.

Once you get the function working, you should remove them. And the third key aspect of the process adds a caveat worth noticing: you might want to remove some of the scaffolding or consolidate multiple statements into compound expressions — but only if it does not make the program difficult to read.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 53-54

37. Picture it: the development version and the finished one

Picture it

The computation is identical. What differs is what is left behind.

Figure (svg): Two columns showing the distance function with development print statements and the final version without them

Same arithmetic. The left column is talking to you; the right one is finished.

A function that prints its intermediate values is unusable inside a larger program, because every call floods the output with noise the caller did not ask for.

38. Worked example: why the scaffolding has to come down

Worked example

Leaving it in does not merely look untidy. Work out the real cost.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    print('dx is', dx)
    dy = y2 - y1
    print('dy is', dy)
    return math.sqrt(dx**2 + dy**2)
UsageWhat the user seesVerdict
one calltwo lines of noisetolerable
called in a loop, 100 timestwo hundred lines of noiseunusable
called from another functionthe caller cannot suppress itthe function is unreusable

Notice the function still works.

Why: It returns the right answer. Nothing about the scaffolding is incorrect.

Consider a caller who does not want the output.

Why: There is no way to turn it off. A function that insists on printing has taken a decision away from its caller — which is exactly the interface-design point from lesson 4b.

Consider scale.

Why: Called a hundred times, it produces two hundred lines nobody asked for, burying whatever the program was actually meant to display.

Figure (svg): The state of the program after each line of Worked example why the scaffolding has to come down, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

The function is correct and unusable. Printing is a decision the caller should make, and scaffolding takes it away — which is why it is removed rather than merely tidied.

Verify: Ask what the equivalent of scaffolding is in the building metaphor.

Why: Scaffolding is essential while building and removed before anybody moves in, because it makes the building unusable for its actual purpose. The metaphor is exact, which is presumably why the book chose it.

39. Sort: scaffolding or part of the function?

Sorting

Ask whether a caller would want this line to run.

Sort into buckets

For a finished distance function, which lines belong and which are scaffolding?

part of the function
dx = x2 - x1; dsquared = dx2 + dy2; return math.sqrt(dsquared)
scaffolding: remove it
print('dx is', dx); print('dsquared is:', dsquared); print('about to return')
keep
Each computes something the return value depends on. Removing any of them would break the function.
scaffold
Each displays an intermediate value for the developer's benefit. Removing all three leaves the function computing exactly the same answer, which is the test for scaffolding.

40. Worked example: consolidating, and when not to

Worked example

The finished function can often be shortened. The book attaches a condition.

# with temporary variables
def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = math.sqrt(dsquared)
    return result

# consolidated
def distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
VersionTradeNote
with variablesfive lines, every step namedeasy to debug
consolidatedone line, no intermediatesharder to debug, easier to read
the ruleconsolidate only if it does not hurt readabilitya judgement

Notice what consolidation removes.

Why: The named intermediate values. dsquared no longer exists, so it cannot be printed if something goes wrong later.

Notice what it adds.

Why: The whole formula visible at once, which for a well-known formula like this is genuinely clearer.

Apply the book's condition.

Why: Consolidate multiple statements into compound expressions, but only if it does not make the program difficult to read. That is a judgement about this particular formula, not a general preference.

Figure (svg): Two columns comparing a function written with named intermediates against the same function as one expression

Neither is right in general. The formula decides.

Both are correct. The consolidated version is better when the formula is familiar enough to read at a glance, and worse when it is not — and the earlier remark that temporary variables can make debugging easier is the other half of the same trade.

Verify: Try the same consolidation on a formula you do not recognise.

Why: A one-line version of an unfamiliar formula is unreadable and undebuggable at once. That the same transformation improves one function and worsens another is what makes it a judgement rather than a rule.

41. Trap: leaving the scaffolding in because it might be useful later

Trap

The trap

A function works, and the developer leaves its print statements in on the grounds that they will help next time something goes wrong.

Treat debugging output as free

Why: It costs nothing to run, and it might be handy.

It is not free. It makes the function unusable inside anything larger, and after a few functions do the same thing the program's real output is invisible among the noise.

The fix

Take the scaffolding down when the function works.

Delete the print statements once each stage is verified

Why: They served their purpose the moment you confirmed the value.

Put them back if you need them again

Why: Retyping two print statements costs seconds. Leaving them in costs the function's reusability permanently.

There is a proper solution to I want this output sometimes — logging, and the exception mechanism in chapter 14 — but the first step is recognising that unconditional printing from inside a function is a problem to be solved rather than a state to settle into.

42. Predict: what happens if scaffolding stays in?

Prediction

The function is correct. Consider what a caller sees.

# distance still prints dx and dy
for i in range(3):
    print(distance(0, 0, i, i))
SourceLines producedNote
each callprints dx and dy, then returns2 noise lines per call
the loop's printprints the returned value1 wanted line per call
total9 lines for 3 results6 of them unwanted

Predict first

How many lines does this loop display in total?

  • Three
  • Six
  • Nine
  • Twelve

Correct: Nine — three calls, each producing two scaffolding lines plus the one line the loop prints.

Why: Two thirds of the output is noise the caller never asked for and cannot switch off. This is the concrete cost of leaving scaffolding in: it scales with the number of calls, so a function that is mildly chatty in isolation becomes unusable in a loop.

43. Two truths and a lie: scaffolding

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. Scaffolding is helpful for building the program but is not part of the final product
  • B. Temporary variables can make debugging easier, so they are not automatically scaffolding
  • C. Scaffolding should be left in, since it does not change what the function returns

Survives elimination: C

Why: C is the lie. It is true that scaffolding does not change the return value — that is exactly why it is scaffolding rather than logic — and false that it should therefore stay. Unconditional printing from inside a function makes it unusable in any larger program, which is a cost that has nothing to do with the return value.

44. Explain it: why start with return 0.0?

Explain it

The step that looks pointless is the one worth defending.

Discussion prompt

A classmate says starting with a function that returns 0.0 is a waste of time, since it obviously does not work. Give them two things that step actually establishes.

Hint: Neither of them is about the mathematics.

Answer:

It establishes that the function exists with the right name and the right parameters, and that calling it works. Those are three ways to be wrong that have nothing to do with the formula.

It also establishes a baseline: from here on, if something breaks, it was broken by the line you just added. Without it, the first run tests everything at once.

The honest framing is that it separates two problems. Making a function exist is easy and making the formula right is hard, and doing them together means debugging both at the same time.

45. Composition: building functions out of functions

Section

Section 5

46. Calling one of your own functions from another

Concept

As you should expect by now, you can call one function from within another — and now that functions return values, the result of one can become the argument of the next.

def circle_area(xc, yc, xp, yp):
    radius = distance(xc, yc, xp, yp)
    result = area(radius)
    return result
LineWhat it producesNote
distance(...)returns the radiusa number
area(radius)returns the areaa number
return resulthands it to the callerthe answer

The temporary variables radius and result are useful for development and debugging, but once the program is working, it can be made more concise by composing the function calls into a single expression.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 54-54

47. Picture it: two functions you already wrote, chained

Picture it

Nothing new is happening. The return value of one call is the argument of the next.

Figure (svg): A call diagram showing circle_area calling distance to get a radius and then area to get the answer

This is lesson 3a's composition, applied to your own functions rather than to the standard library — and it works for the same reason, because a call is an expression with a value.

48. Worked example: composing the two calls

Worked example

The two-variable version and the one-line version. Both are correct.

def circle_area(xc, yc, xp, yp):
    radius = distance(xc, yc, xp, yp)
    result = area(radius)
    return result

def circle_area(xc, yc, xp, yp):
    return area(distance(xc, yc, xp, yp))
VersionShapeNote
with variablesthree lines, two nameseach value checkable
composedone line, no namesthe inner call runs first
equivalenceidentical computationonly the naming differs

Write it with temporary variables first.

Why: The temporary variables radius and result are useful for development and debugging — you can print either one if the answer is wrong.

Compose once it works.

Why: The inner call is evaluated first and its return value becomes the outer call's argument, which is exactly the inside-out rule from lesson 3a.

Apply the same condition as before.

Why: Consolidate only if it does not make the program difficult to read. Here it reads well, because the two function names say what each step does.

Figure (svg): The state of the program after each line of Worked example composing the two calls, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

Both compute the area of a circle from its centre and a point on its perimeter. The composed version is shorter and loses the two named intermediates, which is a trade rather than an improvement.

Verify: Check both versions on a case where the radius is known.

Why: With the centre at the origin and the perimeter point at (3, 4), the radius is 5 and the area is about 78.54. Both versions give the same number, which confirms the composition preserved the computation rather than merely looking equivalent.

49. Predict: which call runs first?

Prediction

Inside-out, as always.

area(distance(0, 0, 3, 4))
StepWhat runsValue
distance(0, 0, 3, 4)the inner call, evaluated first5.0
area(5.0)the outer call, with that value78.539...
returnhands it backthe answer

Predict first

Which function is called first?

  • area
  • distance
  • They run at the same time
  • It depends on the arguments

Correct: distance — it is the inner call, and its return value is what area is given.

Why: The argument is evaluated before the function is called, from lesson 3a, so the inner call must finish before the outer one starts. Reading a composed expression from the inside out is the same habit that lesson introduced, and it applies unchanged to functions you wrote yourself.

50. Worked example: building on functions you trust

Worked example

circle_area assumes distance and area are correct. Notice what that buys.

# already written and tested:
#   distance(x1, y1, x2, y2) -> a distance
#   area(radius)             -> an area

def circle_area(xc, yc, xp, yp):
    return area(distance(xc, yc, xp, yp))
AspectContentConsequence
what is newone linethe composition
what is reusedtwo tested functionsno new arithmetic
where a bug could beonly in the new lineif the others are trusted

Notice how little new code there is.

Why: One line. The Pythagorean theorem and the area formula are both already written, tested, and known to work.

Notice what that does to debugging.

Why: If circle_area is wrong, the fault is in the one new line — the order of the arguments, or which function is called first.

Connect it to incremental development.

Why: This is the same principle at a larger scale: build on something known to work, add a small amount, and check.

Figure (svg): A diagram showing two points entering distance to produce a radius which enters area to produce an answer

Each stage was written and tested separately.

One new line, built on two tested functions. The reuse is not only about typing less — it is about there being only one place a new bug can be.

Verify: Check the argument order deliberately.

Why: distance takes the two points in the order x1, y1, x2, y2, so passing the centre first and the perimeter point second is what the call does. Getting that backwards would still give the right distance, since distance is symmetric — which is worth knowing, because it means this particular mistake would not show up in testing.

51. Trap: composing before either piece is tested

Trap

The trap

A student writes distance, area and circle_area in one sitting and runs circle_area first.

Test at the top, since that is what you actually want

Why: The lower functions exist only to serve it, so testing them separately feels redundant.

A wrong answer could now come from three functions and eleven lines, and the composed call gives no intermediate values to inspect.

The fix

Test each function before anything is built on it.

Check distance against a 3-4-5 triangle

Why: One call, one known answer, and any bug is in five lines.

Check area against a radius whose area you can compute

Why: Radius 1 gives pi, which is instantly recognisable.

Then circle_area has at most one new bug in it, and the leap of faith in the next lesson becomes justified rather than hopeful — you can assume the lower functions work because you checked.

52. Fill the middle: compose two calls

Fill the middle

The two-line version is given. Write it as one expression.

Fill in the blanks

# two lines:
# r = distance(xc, yc, xp, yp)
# return area(r)
# one line:
return area(distance(xc, yc, xp, yp))

Why: The inner call produces the value that r held, and a value is what area needs, so the call can go directly where the variable was. Whether to write it as one line or two is the readability judgement from the previous section — and while developing, the two-line version is easier to debug because r can be printed.

53. Compare: named intermediates versus composition

Comparison

Fill the blanks. Neither is right in general.

Comparison matrix

QuestionNamed intermediatesComposed
Can you print the middle value?yesno — it has no name
How many lines?one per stepone
Better while developing?yesno
Better once it works?only if the names add meaningoften, if it reads well

The bottom row is the honest one: a well-named intermediate is worth keeping even in finished code, and a meaningless one is not.

54. Where composition pays off

Real world

This is how every non-trivial program is built.

Discussion prompt

Think of any task you do in several steps where the output of one step is the input to the next. What would it be like if you had to do the whole thing as one indivisible action, and what does splitting it buy you?

Hint: Cooking, assembling something, processing a form.

Answer:

Splitting buys checkpoints. You can taste the sauce before it goes in, check the measurement before you cut, verify the form before you submit — and a mistake is caught where it happened.

It also buys reuse: a step that is separate can be used in another recipe, and one that is welded into a single action cannot.

Those are the same two benefits as function composition, which is the reason it feels natural once you have it. The programming version adds one thing: the checkpoints can be automated, so the check happens every time rather than when you remember.

55. Compare: void and fruitful functions

Comparison

Fill the blanks. The difference is one statement and everything that follows from it.

Comparison matrix

QuestionVoidFruitful
What does the call produce?Nonethe value of the return expression
Can the caller use the result?no — None is not usefulyes: assign it, or use it in an expression
How does it communicate?by an effect, such as printingby returning a value
Can it be composed?noyes — a call is an expression with a value

The last row is why this chapter matters. Only a fruitful function can be an argument to another function, which is what makes programs buildable out of parts.

56. The procedure: incremental development

Pattern

Six steps, and the first two happen before any real code is written.

  1. Decide what the inputs are — the parameters — and what the output is: the return value and its type.
  2. Write the outline: the def line, and a body that returns a constant of the right type. Run it.
  3. Choose a test case whose answer AND intermediate values you know by hand.
  4. Add a line or two, store each new value in a variable, and print it.
  5. Run it and compare each printed value against what you worked out. Only when it matches, add the next line or two.
  6. When the return value is right, delete the print statements — and consolidate the variables only if the result is still readable.

Step 3 is the step people skip, and it is what makes step 5 possible. A test case whose answer you do not know can tell you that something is wrong, but never which line.

Python documentation — More Control Flow Tools More Control Flow Tools

57. Check yourself 1 of 3: return ends the function

Check

One line never runs.

def f(x):
    return x * 2
    return x * 3
LineWhat happensResult
line 2returns immediatelythe function ends
line 3unreachabledead code
f(5)the first return wins10

Check your understanding

What does f(5) return?

  • A. 10 (correct)
  • B. 15
  • C. Both, as two values
  • D. An error, because there are two return statements

Answer: A

Why: As soon as a return statement runs, the function terminates without executing any subsequent statements. The first return hands back 10 and the second is never reached — it is dead code. Several return statements in a function are perfectly legal and often useful; what matters is that only the first one reached actually runs.

Why B tempts people
This would require the second return to run, which cannot happen because the first one ends the function.
Why C tempts people
A call produces exactly one value. Returning two things at once is possible in Python, using tuples from chapter 12, but that is a single value containing two parts rather than two returns.
Why D tempts people
Multiple return statements are legal and common — the absolute_value function in this lesson has two, one per branch.

58. Check yourself 2 of 3: every path returns

Check

Find the input that reaches no return statement.

def grade(score):
    if score >= 90:
        return 'A'
    if score >= 80:
        return 'B'
InputWhich condition holdsReturn value
score = 95first condition'A'
score = 85second condition'B'
score = 70neitherfalls off the end -> None

Check your understanding

What does grade(70) return?

  • A. 'C'
  • B. None (correct)
  • C. 'B'
  • D. An error

Answer: B

Why: Neither condition is true for 70, so no return statement is reached and execution falls off the end of the function. If the flow of execution gets to the end of a function, the return value is None. The fix is an else branch, which covers everything the conditions excluded and guarantees that every path returns.

Why A tempts people
Nothing in the function mentions a C. The function has only two return statements and neither produces it.
Why C tempts people
The second condition requires a score of at least 80, and 70 does not satisfy it.
Why D tempts people
Falling off the end of a function is not an error. That is precisely what makes it dangerous — it produces a value and continues silently.

59. Check yourself 3 of 3: incremental development

Check

The first version computes nothing on purpose.

Check your understanding

Why does incremental development start with a function that returns a constant?

  • A. Because the constant is often close enough to the right answer
  • B. Because it is syntactically correct and runs, so it can be tested before it gets complicated (correct)
  • C. Because Python requires a return statement in every function
  • D. Because it makes the function faster

Answer: B

Why: The outline obviously does not compute the answer, but it is syntactically correct and it runs, which means you can test it before you make it more complicated. That first test checks the name, the parameters and the call site — three things that have nothing to do with the eventual logic, and three fewer things to suspect when the logic goes wrong.

Why A tempts people
0.0 is not close to any particular distance, and closeness is not the point. The value returned at this stage is irrelevant.
Why C tempts people
Python requires no such thing. A function with no return statement is perfectly legal and returns None.
Why D tempts people
Speed has nothing to do with it. The finished function is the same speed whether or not it was developed incrementally.

60. Where this shows up outside this course

Real world

Incremental development is a general method for building something you cannot get right in one attempt.

Discussion prompt

Think of something you have built or written in stages, checking as you went, rather than completing in one pass. What was your equivalent of returns 0.0, and what did you check at each stage?

Hint: A rough version that is obviously incomplete but definitely works.

Answer:

A recipe scaled up for the first time: cook one portion, taste it, then multiply. The single portion is the version that returns a constant — it does not solve the problem and it proves the method.

Writing works the same way: an outline with one sentence per section is a document that obviously is not finished and can be read end to end, which is exactly what checks the structure before the prose exists.

The common feature is having something complete-but-trivial early, so that every later change is small and every failure is attributable. That is the whole method, and the domain hardly matters.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence. This is the chapter's most common bug.

Predict first

A function computes a value and ends with print(result) instead of return result. What does its caller get?

  • The value, since it was displayed
  • None
  • The value as a string
  • An error, because the function is incomplete

Correct: None — printing displays characters and returns nothing, so the call produces None.

Why: The value was computed and shown, which is exactly what makes this confusing: the evidence on screen says it worked. But displaying and returning go to different places, and a function with no return statement is void. The caller gets None, and the failure usually appears later and elsewhere, as a TypeError mentioning NoneType. If you were tempted by the first option, notice that it is the same mistake as lesson 3c's result = print_twice('Bing') — the same fact, now with a solution available.

62. Explain it to someone else

Explain it

The print-versus-return distinction is what this chapter turns on.

Discussion prompt

A classmate's function prints the right answer, and the program that uses it crashes with something about NoneType. Explain what has happened and what one word fixes it — and say how they could have found it themselves.

Hint: The traceback points at the caller, not at the function.

Answer:

Say: the function prints the value and returns nothing, so the caller receives None. Changing print to return fixes it.

How they could have found it: a TypeError mentioning NoneType nearly always means a void function's result was used as a value, so the thing to look at is not the failing line but the function that produced the value it uses.

It is worth adding the general habit: when a traceback points at a line that looks correct, ask where its VALUES came from. That is lesson 3c's advice about reading a traceback upward, and this is the commonest situation where it pays off.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • The return statement, and the fact that it ends the function immediately
  • Making sure every path through a function reaches a return
  • Incremental development: outline, test, add, test
  • Scaffolding, and knowing when to take it down

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: The return statement itself settles within a few functions. The every-path rule keeps mattering for as long as you write conditionals, and the failure it prevents is silent, which is why the else habit is worth forming now. Incremental development is a method rather than a fact, so it only becomes yours by being used — the honest test is whether your next function starts with an outline. And scaffolding is the easiest of the four to understand and the easiest to neglect, since leaving it in never breaks anything immediately.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw a function as a box with arrows in and out: parameters going in, a return value coming out. Mark on it where a void function differs. Then, below, draw the four stages of building the distance function as four boxes, and write beside each one the value you would check at that stage. Finally, circle the one line in the final version that would not have been there during development, and say why it went.

65. What you can do now

Recap

Four pages, and your functions can finally hand something back.

If you remember one thingIt is this
From returnIt ends the function. Anything after an unconditional one is dead code.
From every-pathA missing return gives None, silently, usually at a boundary value.
From incremental developmentStart with something that runs. Then every failure is caused by the last line you added.
From test casesChoose numbers whose intermediate values you know, not only whose answer you know.
From scaffoldingA function that insists on printing cannot be used inside anything larger.

The next lesson uses return values for something new: functions that return True or False, hiding a complicated test behind a name — and then recursion that computes a value rather than printing one, which is where the leap of faith becomes necessary.

Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-54 — everything on these slides traces back here

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §6.1-6.3, pp. 51-54
  2. Python documentation — More Control Flow Tools
  3. Python documentation — Built-in Functions

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108