Chapter 19

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.

35 minPython 3.12
  1. 1Encounter
  2. 2Understand
  3. 3Worked
  4. 4Predict
  5. 5Apply
  6. 6Stretch

The problem we are solving

Two groups need their average mark:

python
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))
text
58.5
61.33

The 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 return from print — 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

python
def average(values):
    return round(sum(values) / len(values), 2)


print(average([72, 45]))
print(average([90, 33, 61]))
text
58.5
61.33

The 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 like if or for
  • return — 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.

python
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)
text
10
10
None
11

The 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

python
def check(mark):
    if mark >= 40:
        return "pass"
    return "fail"


print(check(72))
print(check(20))
text
pass
fail

The 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:

python
def average(values):
    return sum(values) / len(values)


print(average([]))
text
ZeroDivisionError: division by zero

The fix goes at the very top of the function:

python
def average(values):
    if not values:
        return 0.0
    return round(sum(values) / len(values), 2)


print(average([72, 45]))
print(average([]))
text
58.5
0.0

if 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.

Is 0.0 the right answer? Debatable. It conflates "nobody has a mark" with "everybody scored zero". Real programs often return None, or raise an error using chapter twenty-four's raise. 0.0 is 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:

python
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__)
text
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:

python
# 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))
text
rafi      3 papers  avg  73.67  grade B
ahmed     2 papers  avg   41.5  grade C
dia       0 papers  avg    0.0  grade F

Four 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.