Functions — one decision, one name
Writing functions with def, what separates return from print, handling edge cases with a guard at the top, and docstrings.
- 1Encounter
- 2Understand
- 3Worked
- 4Predict
- 5Apply
- 6Stretch
The problem we are solving
Two groups need their average mark:
marks_a = [72, 45]
marks_b = [90, 33, 61]
print(round(sum(marks_a) / len(marks_a), 2))
print(round(sum(marks_b) / len(marks_b), 2))58.5
61.33The two lines are almost identical; only the name differs. A third group means a third line, a fourth means a fourth.
The problem is not the typing — copying is cheap. The problem is that one decision is now written down in several places. Asked to show the average to one decimal place instead of two, you have to find and change every line, and missing one leaves the program behaving two different ways — with no error.
And what about an empty list? sum([]) / len([]) divides by zero and the program stops. That check would have to be added to each line separately.
A function is a way of writing one decision once and giving it a name. After that, calling the name does the work, and changing it means changing one place.
By the end of this chapter you can
- Write and call a function with
def - Say what separates
returnfromprint— the most important thing here - Keep a function small and limited to one job
- Handle edge cases with a guard at the top
- Write a docstring
Prerequisites: Comprehensions.
A first function
def average(values):
return round(sum(values) / len(values), 2)
print(average([72, 45]))
print(average([90, 33, 61]))58.5
61.33The shape:
def— "I am defining a function"average— the name, following the same rules as a variable name(values)— the parameter, the door data comes in through:and an indented block — the body, exactly likeiforforreturn— sending a result back
Writing average([72, 45]) is calling it. [72, 45] is the argument, what is actually sent; values is the parameter, the name inside. Two different words, and both turn up in error messages, so the distinction is worth keeping.
Two blank lines. Leaving two blank lines after a function definition is the Python convention. Python runs happily with one, but all Python code is written this way, so it is worth the habit.
return and print are not the same
This is the most important section in the chapter, and beginners' largest confusion.
def with_return(x):
return x * 2
def with_print(x):
print(x * 2)
a = with_return(5)
b = with_print(5)
print(a)
print(b)
print(a + 1)10
10
None
11The first two 10s on screen make the functions look identical. They are not.
with_return handed the value back, so a holds 10 and can be added to — hence 11 on the last line.
with_print displayed the value and handed nothing back, so b is None. The number appeared on screen, and the program received nothing. b + 1 would raise a TypeError.
Hold the rule this way: print is for a person, return is for the program. A function that calculates should return; displaying happens where it was called. That way the same function can serve a screen one day and a file the next.
Without a return, a function silently returns None — and that is the same None that append and .sort() handed back in chapters twelve and thirteen. Now you can see why they did: their job was to change something, not to hand anything back.
return stops immediately
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))
print(check(20))pass
failThe moment return runs, the function is over — the lines after it never run. Which is why no else was needed here: if the first return happens, the second line is unreachable.
This shape — return as soon as a condition matches — flattens the deep if/else trees of chapter nine, and makes code much easier to read.
Handling the edges — a guard
Remember the problem this chapter opened with? The empty list:
def average(values):
return sum(values) / len(values)
print(average([]))ZeroDivisionError: division by zeroThe fix goes at the very top of the function:
def average(values):
if not values:
return 0.0
return round(sum(values) / len(values), 2)
print(average([72, 45]))
print(average([]))58.5
0.0if not values is chapter eight's truthiness rule again: an empty list is false, so not makes it true. No need to write len(values) == 0.
This is called a guard: deal with the abnormal case at the top, then write the rest without worrying. Functions make this cheap — the check is written once and every caller benefits.
Is0.0the right answer? Debatable. It conflates "nobody has a mark" with "everybody scored zero". Real programs often returnNone, or raise an error using chapter twenty-four'sraise.0.0is kept here for simplicity — but know that it is a decision rather than the obvious answer.
Docstrings
A piece of text on the function's first line becomes its documentation:
def average(values):
"""Return the mean of values, or 0.0 when there are none."""
if not values:
return 0.0
return round(sum(values) / len(values), 2)
print(average([1, 2, 3]))
print(average.__doc__)2.0
Return the mean of values, or 0.0 when there are none.What separates it from a comment is that it survives into the running program — editors show it, and help(average) prints it.
A good docstring says what comes back and what happens at the edges, not how the code works. The code is right there underneath.
A complete example
report.py:
# Three small functions, each doing one thing
def average(values):
"""Return the mean of values, or 0.0 when there are none."""
if not values:
return 0.0
return round(sum(values) / len(values), 2)
def grade(mark):
"""Turn a mark into a letter."""
if mark >= 80:
return "A"
if mark >= 70:
return "B"
if mark >= 40:
return "C"
return "F"
def summarise(name, marks):
"""Build one report line for a person."""
mean = average(marks)
return f"{name:<8} {len(marks):>2} papers avg {mean:>6} grade {grade(mean)}"
people = {
"rafi": [72, 88, 61],
"ahmed": [45, 38],
"dia": [],
}
for name, marks in people.items():
print(summarise(name, marks))rafi 3 papers avg 73.67 grade B
ahmed 2 papers avg 41.5 grade C
dia 0 papers avg 0.0 grade FFour things worth noticing.
Each function does one job. average averages, grade gives a letter, summarise builds a line. None of them prints — printing happens in the loop at the bottom, in one place. So writing this report to a file instead means changing that one line.
Functions call functions. summarise calls average and grade. This is the real benefit: large work assembled from small, dependable pieces.
grade has no else. Every return stops the function, so the conditions stay flat. Chapter nine's rule still applies: strictest condition first — putting 40 before 80 would give everybody a C.
dia's empty list did not break anything. The guard returned 0.0, grade turned that into F, and the report line printed normally. The edge case was handled once, in one place.
When it breaks
NameError: name 'greet' is not defined — but the function is right there It was called before its def line ran. Python reads a file top to bottom, and the name does not exist until the def executes. Keep all defs at the top and the calling code below.
TypeError: average() missing 1 required positional argument: 'values' No argument was given — average() instead of average(marks). The message even names the missing parameter.
The function returns None There is no return, or there is one at the wrong indentation — inside an if whose condition does not match, say. Another common cause: print was written where return was meant.
A variable inside the function is not visible outside That is expected — why is chapter twenty-one's subject. For now: return whatever is needed outside.
SyntaxError — no colon on the def line Like if and for, a def line ends with a colon.
IndentationError: expected an indented block after function definition A def needs at least one indented line after it.
Step 4 of 6 — Predict
Check your understanding
The function prints rather than returns. What do the two lines print?
def double(x):
print(x * 2)
result = double(5)
print(result)- A10 None
- B10 10
- CNone 10
- D10
Two returns and no else. What is printed?
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))- Apass
- Bfail
- Cpass fail
- DNone
The function is right there in the file. What happens even so?
print(greet("rafi"))
def greet(name):
return "hello " + name- AA `NameError` — the function was called before its `def` line ran
- Bhello rafi
- CA `TypeError`
- DA `SyntaxError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
Your turn
Write a file called text_tools.py with four functions, each carrying a docstring, and none of them printing anything:
clean(text)— trims spaces and lowercasesinitials(full_name)— turns"Rafi Ahmed"into"RA"word_count(text)— counts words, giving0for empty textsummarise(text)— calls the other three and returns one readable line
Then at the bottom of the file, call summarise on a few pieces of text and print the results — that is the only place anything is printed.
Finally, three experiments:
- Call
word_count(""). What would have happened without the guard? - Replace
summarise'sreturnwith aprint, then store its result in a variable and print that. What do you see, and why? - Move the call to
cleanabove its owndef, and read the error.
The second is the most valuable three minutes in this chapter. Seeing the difference between print and return once with your own eyes means never confusing them again.
Step 6 of 6
Stretch — the chapter quiz
Ten questions from easy to hard. The last ones are difficult on purpose.
Sign in to take the quiz