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
Title
Python · Chapter 6 — Fruitful functions
§6.1-6.3, pp. 51-54
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
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.
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
Think Python, 2nd edition — Allen B. Downey §6.1-6.3, pp. 51-51
Section
Section 1
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| Line | What happens | Effect |
|---|---|---|
| a = math.pi * radius**2 | compute the area into a local variable | a holds the value |
| return a | end the function, hand back a | the call produces that value |
| the caller | can assign it, print it, use it | the 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
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.
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| Use | Why it works | Result |
|---|---|---|
| area(3) at the prompt | an expression, so its value is displayed | 28.27... |
| a = area(3) | the value is assigned | a holds it |
| area(3) + area(4) | used inside a larger expression | the 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
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.
Prediction
The function prints. Decide what the caller receives.
def area(r):
print(math.pi * r**2)
a = area(3)| Step | What happens | State |
|---|---|---|
| the body | displays the area | 28.27... appears |
| no return statement | the function is void | returns None |
| a | gets None | a -> None |
Predict first
What does a refer to after this runs?
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.
Worked example
Not merely sets the answer. Predict what the second print does.
def f(x):
print('before')
return x * 2
print('after')| Line | What happens | Note |
|---|---|---|
| print('before') | runs normally | before |
| return x * 2 | the function ENDS here | the value is handed back |
| print('after') | never reached | dead 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.
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.
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.
Discrimination
Look for a return statement with an expression.
Sort into buckets
For each function body, is it fruitful or void?
Prediction
Everything after a reachable return is unreachable.
def f(x):
if x > 0:
return 'positive'
return 'not positive'
print('done')| Line | When it is reached | Verdict |
|---|---|---|
| line 3 | returns only when x > 0 | reachable |
| line 4 | reached when the condition was false | reachable |
| line 5 | after a return that always runs | dead |
Predict first
Which line is dead code?
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.
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.
Section
Section 2
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| Input | Which branch | Return value |
|---|---|---|
| x = -5 | the first branch | returns 5 |
| x = 5 | the else branch | returns 5 |
| x = 0 | the else branch | returns 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
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.
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| Input | Which condition holds | Return value |
|---|---|---|
| x = -5 | first condition true | returns 5 |
| x = 5 | second condition true | returns 5 |
| x = 0 | neither condition true | returns 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
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.
Prediction
The condition is true. Trace what happens after the return.
def f(x):
if x > 0:
return 'positive'| Input | What happens | Return value |
|---|---|---|
| x = 5 | the condition is true | returns 'positive' |
| x = -5 | the condition is false | falls off the end |
| x = -5 result | no return was hit | None |
Predict first
What does f(-5) return?
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.
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| Case | Which branch | Return value |
|---|---|---|
| x > y | first branch | 1 |
| x == y | second branch | 0 |
| otherwise | the 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
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.
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.
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.
Error analysis
Three inputs, one of which is not handled. Mark the fault and the fix.
Annotate
Zero is the boundary of both conditions, and boundary values are exactly where fall-through bugs live.
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.
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.
Section
Section 3
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| Aspect | Assessment | Note |
|---|---|---|
| what it computes | nothing — always zero | obviously incomplete |
| what it is | syntactically correct and runnable | testable |
| why that matters | you can test it before making it complicated | the 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
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
The values 3, 4, 25 and 5 are not accidental: the test case was chosen so that every intermediate value is known in advance.
Worked example
The book picks its numbers deliberately. Work out why these ones.
>>> distance(1, 2, 4, 6)
0.0| Intermediate value | Calculation | Known answer |
|---|---|---|
| x2 - x1 | 4 - 1 | 3 |
| y2 - y1 | 6 - 2 | 4 |
| the distance | the hypotenuse of a 3-4-5 triangle | 5 |
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 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.
Ranking
Each stage is runnable, and each is checked before the next.
Put in order
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.
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| Stage | What was added | What to check |
|---|---|---|
| stage 1 | return 0.0 | confirms it runs |
| stage 2 | compute dx and dy, print them | should show 3 and 4 |
| stage 3 | compute dsquared, print it | should show 25 |
| stage 4 | take the square root and return it | should 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
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.
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.
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.
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| Value | Calculation | Expected |
|---|---|---|
| dx | 4 - 1 | 3 |
| dy | 6 - 2 | 4 |
| the return | still the placeholder | 0.0 |
Predict first
For distance(1, 2, 4, 6), what should this stage display?
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.
Matching
The book names three. Each addresses a different failure.
Match the pairs
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.
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.
Section
Section 4
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
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
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.
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)| Usage | What the user sees | Verdict |
|---|---|---|
| one call | two lines of noise | tolerable |
| called in a loop, 100 times | two hundred lines of noise | unusable |
| called from another function | the caller cannot suppress it | the 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 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.
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?
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)| Version | Trade | Note |
|---|---|---|
| with variables | five lines, every step named | easy to debug |
| consolidated | one line, no intermediates | harder to debug, easier to read |
| the rule | consolidate only if it does not hurt readability | a 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
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.
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.
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.
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))| Source | Lines produced | Note |
|---|---|---|
| each call | prints dx and dy, then returns | 2 noise lines per call |
| the loop's print | prints the returned value | 1 wanted line per call |
| total | 9 lines for 3 results | 6 of them unwanted |
Predict first
How many lines does this loop display in total?
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.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
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.
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.
Section
Section 5
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| Line | What it produces | Note |
|---|---|---|
| distance(...) | returns the radius | a number |
| area(radius) | returns the area | a number |
| return result | hands it to the caller | the 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
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.
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))| Version | Shape | Note |
|---|---|---|
| with variables | three lines, two names | each value checkable |
| composed | one line, no names | the inner call runs first |
| equivalence | identical computation | only 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
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.
Prediction
Inside-out, as always.
area(distance(0, 0, 3, 4))| Step | What runs | Value |
|---|---|---|
| distance(0, 0, 3, 4) | the inner call, evaluated first | 5.0 |
| area(5.0) | the outer call, with that value | 78.539... |
| return | hands it back | the answer |
Predict first
Which function is called first?
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.
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))| Aspect | Content | Consequence |
|---|---|---|
| what is new | one line | the composition |
| what is reused | two tested functions | no new arithmetic |
| where a bug could be | only in the new line | if 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
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.
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.
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.
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.
Comparison
Fill the blanks. Neither is right in general.
Comparison matrix
| Question | Named intermediates | Composed |
|---|---|---|
| Can you print the middle value? | yes | no — it has no name |
| How many lines? | one per step | one |
| Better while developing? | yes | no |
| Better once it works? | only if the names add meaning | often, 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.
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.
Comparison
Fill the blanks. The difference is one statement and everything that follows from it.
Comparison matrix
| Question | Void | Fruitful |
|---|---|---|
| What does the call produce? | None | the value of the return expression |
| Can the caller use the result? | no — None is not useful | yes: assign it, or use it in an expression |
| How does it communicate? | by an effect, such as printing | by returning a value |
| Can it be composed? | no | yes — 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.
Pattern
Six steps, and the first two happen before any real code is written.
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
Check
One line never runs.
def f(x):
return x * 2
return x * 3| Line | What happens | Result |
|---|---|---|
| line 2 | returns immediately | the function ends |
| line 3 | unreachable | dead code |
| f(5) | the first return wins | 10 |
Check your understanding
What does f(5) return?
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.
Check
Find the input that reaches no return statement.
def grade(score):
if score >= 90:
return 'A'
if score >= 80:
return 'B'| Input | Which condition holds | Return value |
|---|---|---|
| score = 95 | first condition | 'A' |
| score = 85 | second condition | 'B' |
| score = 70 | neither | falls off the end -> None |
Check your understanding
What does grade(70) return?
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.
Check
The first version computes nothing on purpose.
Check your understanding
Why does incremental development start with a function that returns a constant?
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.
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.
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?
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.
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.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
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.
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.
Recap
Four pages, and your functions can finally hand something back.
| If you remember one thing | It is this |
|---|---|
| From return | It ends the function. Anything after an unconditional one is dead code. |
| From every-path | A missing return gives None, silently, usually at a boundary value. |
| From incremental development | Start with something that runs. Then every failure is caused by the last line you added. |
| From test cases | Choose numbers whose intermediate values you know, not only whose answer you know. |
| From scaffolding | A 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
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.