Python Book · Chapter 7

More functions

Chapter 5 covered the basics. This chapter shows the features that make Python functions flexible: any number of arguments, functions passed around like values, and functions that call themselves.

Any number of arguments

A parameter written as *args collects any extra positional arguments into a tuple:

def total(*numbers):
    result = 0
    for n in numbers:
        result += n
    return result

total(1, 2)         # 3
total(1, 2, 3, 4)   # 10
total()             # 0

**kwargs does the same for keyword arguments, collecting them into a dictionary:

def describe(**info):
    for key, value in info.items():
        print(f"{key} = {value}")

describe(name="Ada", year=2)
# name = Ada
# year = 2

The names args and kwargs are just conventions; the * and ** are what matter.

Unpacking when you call

The stars also work the other way, spreading a list or dictionary out into separate arguments:

point = [3, 4]
print(*point)   # same as print(3, 4)

options = {"sep": "-", "end": "!\n"}
print("a", "b", **options)   # a-b!

Keyword-only arguments

Parameters after a bare * must be passed by name. This makes calls readable and stops arguments being mixed up:

def connect(host, *, port=443, timeout=10):
    return f"{host}:{port} (timeout {timeout}s)"

connect("example.com", port=8080)   # fine
connect("example.com", 8080)        # TypeError

Functions are values

A function can be stored in a variable, put in a list or passed to another function, just like a number or a string:

def shout(text):
    return text.upper() + "!"

def whisper(text):
    return text.lower() + "..."

def apply(style, text):
    return style(text)

apply(shout, "hello")     # 'HELLO!'
apply(whisper, "HELLO")   # 'hello...'

Notice there are no brackets after shout when passing it in. shout is the function itself; shout("hi") calls it.

Lambdas and sorting with a key

A lambda is a small function written in one expression, handy when you need a function for a single use. The most common place you will meet one is the key argument of sorted, min and max:

words = ["banana", "kiwi", "apple", "fig"]
sorted(words)                          # alphabetical
sorted(words, key=len)                 # ['fig', 'kiwi', 'apple', 'banana']
sorted(words, key=lambda w: w[-1])     # by last letter

students = [("Ada", 88), ("Linus", 95), ("Grace", 91)]
max(students, key=lambda s: s[1])      # ('Linus', 95)

key is called once on every item, and items are compared by what it returns. If a lambda gets longer than one short line, use a normal def instead.

Recursion

A recursive function calls itself on a smaller version of the problem. Every recursive function needs a base case that stops it:

def factorial(n):
    if n <= 1:          # base case
        return 1
    return n * factorial(n - 1)

factorial(5)   # 120

Recursion fits naturally when the data is itself nested, such as folders inside folders or trees. Python stops recursion after about 1,000 levels with a RecursionError, so for simple counting a loop is usually better.

Type hints

You can note the types a function expects and returns. Python does not enforce them, but editors and tools like mypy use them to catch mistakes, and they document your code:

def average(scores: list[float]) -> float:
    return sum(scores) / len(scores)

Exercises

1. Average of anything

Write average(*nums) that returns the average of any number of arguments, or None if there are none.

Show answer
def average(*nums):
    if not nums:
        return None
    return sum(nums) / len(nums)

average(2, 4, 9)   # 5.0
average()          # None

2. Sort by two things

Sort ["pear", "fig", "apple", "kiwi", "date"] by length, and alphabetically when two words have the same length.

Show answer
words = ["pear", "fig", "apple", "kiwi", "date"]
sorted(words, key=lambda w: (len(w), w))
# ['fig', 'date', 'kiwi', 'pear', 'apple']

Tuples compare item by item, so the key sorts by length first and uses the word itself to break ties.

3. Recursive sum of digits

Write digit_sum(n) recursively for a non-negative integer, without converting it to a string.

Show answer
def digit_sum(n):
    if n < 10:
        return n
    return n % 10 + digit_sum(n // 10)

digit_sum(4096)   # 19

The base case is a single digit. Each call handles the last digit and passes the rest on.