Chapter 7
Functions
Every chapter so far has repeated the same few lines of logic in different
examples: build an entry, loop over expenses, add up amounts. A
function lets you write that logic once, give it a name, and call it as
many times as you need. This chapter writes the first real functions this
book's tracker is built from.
Defining and calling
def starts a function definition:
def greet():
print("Hello.")
greet()
Hello.greet is the function's name. The empty parentheses mean it takes no
inputs. greet(), with parentheses, calls it, running the indented block
underneath. Without the parentheses, greet just refers to the function
itself, without running it.
A function usually takes input through parameters, named inside the parentheses:
def shout(message):
print(message.upper())
shout("watch out")
WATCH OUTmessage is a parameter. "watch out" is the argument, the actual
value passed in when you call it. The words are related but distinct:
message is the name the function uses internally; "watch out" is what
you handed it this time.
return
print() shows a value; return hands one back to whoever called the
function, so it can be used in more code:
def double(n):
return n * 2
result = double(21)
print(result)
42A function without a return hands back None:
def greet():
print("Hello.")
result = greet()
print(result)
Hello.
Nonegreet() printed "Hello." when it ran, then result held None because
there was nothing to return. print() and return do different jobs:
print() shows something to whoever's watching the terminal; return gives
a value back to the code that called the function. This book's real
functions almost always return rather than print, so the caller decides
what to do with the answer.
Functions for this book's tracker
def add_expense(expenses, amount, category, note):
expenses.append({"amount": amount, "category": category, "note": note})
return expenses
expenses = []
add_expense(expenses, 4.50, "coffee", "flat white")
add_expense(expenses, 12.00, "transit", "monthly pass")
print(expenses)
[{'amount': 4.5, 'category': 'coffee', 'note': 'flat white'}, {'amount': 12.0, 'category': 'transit', 'note': 'monthly pass'}]def total(expenses):
running = 0
for entry in expenses:
running += entry["amount"]
return running
print(total(expenses))
16.5def expenses_over(expenses, amount):
matches = []
for entry in expenses:
if entry["amount"] > amount:
matches.append(entry)
return matches
big = expenses_over(expenses, 10)
print(len(big))
print(big[0]["category"])
1
transitEach of these does one clear job, is named for what it does, and can be tested on its own with any list of entries you hand it, not just this particular one. That's worth noticing already; it matters a lot by the end of this chapter.
Default argument values
A parameter can have a default value, used when the caller doesn't supply one:
def add_expense(expenses, amount, category, note=""):
expenses.append({"amount": amount, "category": category, "note": note})
return expenses
expenses = []
add_expense(expenses, 4.50, "coffee")
print(expenses)
[{'amount': 4.5, 'category': 'coffee', 'note': ''}]Leaving out note used the default, "", matching this book's entry shape:
"note" is always present, even when it's empty.
Keyword arguments
Any argument can be passed by name instead of position, which is especially useful once a function takes several:
def add_expense(expenses, amount, category, note=""):
expenses.append({"amount": amount, "category": category, "note": note})
return expenses
expenses = []
add_expense(expenses, amount=32.10, category="groceries", note="weekly shop")
print(expenses)
[{'amount': 32.1, 'category': 'groceries', 'note': 'weekly shop'}]Passed by name like this, the order no longer matters, and the call reads clearly even without looking at the function's definition.
Documenting a function
A docstring, a string literal right under the def line, documents what
a function does. Python treats it specially: it's stored on the function and
help() can show it back to you.
def total(expenses):
"""Return the sum of every entry's amount."""
running = 0
for entry in expenses:
running += entry["amount"]
return running
print(total.__doc__)
Return the sum of every entry's amount.Not every function needs one, a short, obviously named function like
double(n) rarely does, but a function whose purpose isn't obvious from its
name and parameters alone is worth one line explaining what it returns. This
book's own functions from here on include a docstring whenever the name
alone doesn't say enough.
A function that takes any number of arguments
Every function so far takes a fixed number of parameters. Python also lets a
function accept any number of positional arguments, collected into a tuple,
using a single * before the parameter name:
def add_amounts(*amounts):
return sum(amounts)
print(add_amounts(4.50, 12.00))
print(add_amounts(4.50, 12.00, 32.10))
16.5
48.6*amounts gathers however many arguments were passed, two here, three
there, into one tuple named amounts inside the function. The matching tool
for keyword arguments is **kwargs, gathering them into a dict instead.
Both are common in library code you'll read (you've already called
functions that use this under the hood, like print() accepting any number
of arguments), but this book's own functions stick to named parameters,
which stay easier to read when you're still getting comfortable with
functions in general. Treat *args and **kwargs as a "next," not a gap:
you'll recognize them on sight now, and write your own once fixed parameter
lists start to feel limiting.
The mutable default argument trap
Here's a version of add_expense that looks reasonable and has a real bug:
def add_expense(amount, category, expenses=[]):
expenses.append({"amount": amount, "category": category, "note": ""})
return expenses
first_trip = add_expense(4.50, "coffee")
second_trip = add_expense(12.00, "transit")
print(first_trip)
[{'amount': 4.5, 'category': 'coffee', 'note': ''}, {'amount': 12.0, 'category': 'transit', 'note': ''}]first_trip has two entries, including the transit one it was never
handed. The Python documentation is direct about why: "the default value is
evaluated only once. This makes a difference when the default is a mutable
object such as a list, dictionary, or instances of most classes." That empty
[] is built exactly once, when the function is defined, and every call
that doesn't supply its own expenses shares that same list. This is the
single most common surprise a new Python programmer runs into, and it's
worth remembering by name: never use a mutable value, a list or a dict, as a
default argument.
The fix is to default to None and build a fresh list inside the function
when needed:
def add_expense(amount, category, expenses=None):
if expenses is None:
expenses = []
expenses.append({"amount": amount, "category": category, "note": ""})
return expenses
first_trip = add_expense(4.50, "coffee")
second_trip = add_expense(12.00, "transit")
print(first_trip)
print(second_trip)
[{'amount': 4.5, 'category': 'coffee', 'note': ''}]
[{'amount': 12.0, 'category': 'transit', 'note': ''}]Now each call that doesn't supply its own list gets a genuinely fresh one.
None itself is immutable, so it's safe to share as a default; the trap is
specifically about sharing something changeable.
Why functions help
Three reasons, all visible already in this chapter's examples. Naming: total(expenses) says what it computes, instead of a loop you have to read to understand. Reuse: the same total() works on any list of entries, not just one particular one. And testability: every function above takes its inputs as arguments and hands back a result with return, with no input() or hidden state involved, which means you can check exactly what it does by calling it with known values and looking at what comes back, the way this book's own build checks every example. Chapter 11 leans on this directly: the whole tracker script is built almost entirely from functions exactly like these, and the only part that isn't checkable this way is the handful of lines that actually talk to a person.
Practice
Try each of these before you read the solution under it.
- Write
average(expenses)that returns the average amount across a list of entries. Use it on a list of three entries you build yourself. - Write
category_total(expenses, category)that returns the sum of amounts for entries matching a given category. - Find and fix the mutable-default bug in this function, keeping the same
two parameters and behavior otherwise:
def with_tax(amount, seen=[]): seen.append(amount * 1.08); return seen - Write
largest(*amounts)using*amounts, returning the single largest value passed in. Call it with three different amounts.
Solutions
1. Build on total() from earlier in the chapter.
def total(expenses):
running = 0
for entry in expenses:
running += entry["amount"]
return running
def average(expenses):
return total(expenses) / len(expenses)
sample = [
{"amount": 4.50, "category": "coffee", "note": ""},
{"amount": 12.00, "category": "transit", "note": ""},
{"amount": 32.10, "category": "groceries", "note": ""},
]
print(average(sample))
16.22.
def category_total(expenses, category):
running = 0
for entry in expenses:
if entry["category"] == category:
running += entry["amount"]
return running
sample = [
{"amount": 4.50, "category": "coffee", "note": ""},
{"amount": 8.75, "category": "coffee", "note": ""},
{"amount": 12.00, "category": "transit", "note": ""},
]
print(category_total(sample, "coffee"))
13.253. Same fix as the tracker example: default to None, build the list
inside.
def with_tax(amount, seen=None):
if seen is None:
seen = []
seen.append(amount * 1.08)
return seen
first = with_tax(10)
second = with_tax(20)
print(first)
print(second)
[10.8]
[21.6]4.
def largest(*amounts):
return max(amounts)
print(largest(4.50, 32.10, 12.00))
32.1Where this leaves you
You can write a function with parameters, a return value, and default
arguments, call it with positional or keyword arguments, and you know the
mutable-default trap by name and by the fix. You've also seen why a function
with no input() in it is easy to check, which is exactly what makes
Chapter 11's closing script possible. Chapter 8 uses import to bring in
tools you didn't have to write yourself.