Make a day10 folder for today’s files, open it in your editor, and run each script from its terminal, as on earlier days.
On Day 9 you wrote your own functions: def, parameters, return. Every function you wrote needed exactly as many arguments as it had parameters, given in exactly the right order. The built-in functions you have used since Day 1 are more flexible than that. round(7.6) and round(7.6, 1) both work. enumerate(word) counts from 0 and enumerate(word, 1) counts from 1. max takes two numbers, five numbers, or a whole list. Today you learn how to give your own functions that flexibility, and then something new: handing a function to another function, so you can tell sorted(), min(), and max() how to compare things. By the end of the day you will be able to read the first line of any function in the Python documentation and know how to call it.
10.1 Default Parameter Values
Day 9’s functions always needed every argument. Suppose you write a greeting function and nearly every call says “Hello”. Typing "Hello" in every call is repetitive, and leaving it out is an error. What you want is an argument the caller can leave out, with a sensible value used in its place. round already works like this: with no second argument it rounds to a whole number.
Recall from Day 9 that a parameter is a name in the function’s definition and an argument is the value you hand over in a call. A parameter can be given a default value: a value written in the def line with =, which Python uses whenever the call leaves that argument out.
def name(required, optional=default_value):
requiredhas no default, so every call must supply it. It is a required parameter.optional=default_valuehas a default, so a call may leave it out. It is an optional parameter.- There are no spaces around the
=in a parameter list. That is the PEP 8 style for defaults (and for keyword arguments in 10.2), and it helps you tell a default apart from an ordinary assignment.
Worked example: A greeting with a default
def greet(name, greeting="Hello"):
"""Return a greeting for name."""
return f"{greeting}, {name}!"
print(greet("Ana"))
print(greet("Ben", "Welcome"))Hello, Ana!
Welcome, Ben!- Line 6 passes one argument. Python matches
"Ana"toname. Nothing is left forgreeting, so Python uses its default,"Hello". - Line 7 passes two.
"Ben"goes tonameand"Welcome"goes togreeting, replacing the default for this call only. The next call that leavesgreetingout gets"Hello"again.
Now change what the default is: a number rather than a string, used in a calculation.
def power(base, exponent=2):
"""Return base raised to exponent. With no exponent, square it."""
return base ** exponent
print(power(5))
print(power(2, 10))
print(power(10, 0))25
1024
1
power(5) uses the default exponent 2, giving 5 squared. power(2, 10) gives 2 to the power 10. power(10, 0) is worth a look: the caller passed 0, a value that is falsy (Day 4), but Python still uses it. A default is used only when the argument is missing, never because the value passed looks empty.
Key idea
def f(a, b=2): makes b optional: a call that leaves it out gets 2, and a call that passes it replaces 2 for that
call only. Parameters without defaults are required.
Watch out
A default before a required parameter. It seems natural to put the greeting first, since it comes first in the sentence:
def greet(greeting="Hello", name):
"""Return a greeting for name."""
return f"{greeting}, {name}!"
print(greet("Ana")) File ".../day10/default_first.py", line 1
def greet(greeting="Hello", name):
^^^^
SyntaxError: parameter without a default follows parameter with a defaultPython matches arguments to parameters from left to right. If greet("Ana") were allowed, Python could not tell whether "Ana" was meant for greeting (leaving name missing) or for name (skipping greeting). To make every call unambiguous, the rule is: required parameters come first, then the ones with defaults. Fix it by swapping them back: def greet(name, greeting="Hello"):. Older Python versions word this message differently, but it is still a SyntaxError on the def line.
Pause and try
Question 1 · Tracing
What does this program print? For each call, say which values seats and price get and where they came from.
def ticket(event, seats=1, price=12.5):
"""Return a line describing a ticket order."""
total = seats * price
return f"{event}: {seats} x {price} = {total}"
print(ticket("Concert"))
print(ticket("Play", 3))
print(ticket("Film", 2, 8))Check your reasoning
Concert: 1 x 12.5 = 12.5
Play: 3 x 12.5 = 37.5
Film: 2 x 8 = 16| Call | seats |
price |
total |
|---|---|---|---|
ticket("Concert") |
1 (default) | 12.5 (default) | 12.5 |
ticket("Play", 3) |
3 (passed) | 12.5 (default) | 37.5 |
ticket("Film", 2, 8) |
2 (passed) | 8 (passed) | 16 |
Arguments fill parameters from the left, so the single number in ticket("Play", 3) goes to seats, not price. In the last call price is the int 8, so 2 * 8 is the int 16, with no .0.
Reread 10.1 Default Parameter Values if your output differed.
10.2 Keyword Arguments
So far every argument has been matched to a parameter by its position: first argument to first parameter, second to second. That works for one or two parameters. With three similar ones it becomes easy to put them in the wrong order, and Python cannot notice, because a string is a string.
Worked example: Mixing up the order
def make_badge(name, role, team):
"""Return the text for a name badge."""
return f"{name} | {role} | {team}"
print(make_badge("Ana", "Captain", "Blue"))
print(make_badge("Ana", "Blue", "Captain"))
print(make_badge(team="Blue", name="Ana", role="Captain"))Ana | Captain | Blue
Ana | Blue | Captain
Ana | Captain | Blue- Line 6 gives the arguments in the order of the parameters, so the badge is right.
- Line 7 swaps the last two. Python happily puts
"Blue"inroleand"Captain"inteam. There is no error, just a wrong badge. - Line 8 names each parameter in the call:
team="Blue"means “the parameter calledteamgets"Blue"”. The arguments are in a completely different order, and the badge is still right.
An argument matched by its place in the call is a positional argument. Day 6 met one other kind, reverse=True in sorted: a keyword argument, written as parameter_name=value in a call. A keyword argument is matched by name, so its position does not matter. Keyword arguments also make a call easier to read, because each value says what it is for.
Keyword arguments really pay off with defaults. In this coffee order, both size and milk are optional. With positional arguments alone, there would be no way to choose the milk without also typing the size, because milk comes second. A keyword argument can skip over a default:
def order(drink, size="medium", milk="whole"):
"""Return a description of a coffee order."""
return f"A {size} {drink} with {milk} milk"
print(order("latte"))
print(order("latte", "large"))
print(order("latte", milk="oat"))
print(order("mocha", milk="oat", size="small"))A medium latte with whole milk
A large latte with whole milk
A medium latte with oat milk
A small mocha with oat milk
order("latte")uses both defaults.order("latte", "large")fillssizeby position.order("latte", milk="oat")fillsdrinkby position, skipssize(it keeps its default), and setsmilkby name.- The last call gives two keyword arguments in the “wrong” order, which is fine: names, not places, decide where they go.
You have been close to keyword arguments since Day 1. print has two optional parameters that change what it does: sep, the text put between the values (a space by default), and end, the text put after them (a newline by default). You can only set them by keyword:
print("2026", "10", "15", sep="-")
print("a", "b", "c", sep="")
print("Loading", end="")
print("...", end="")
print(" done")2026-10-15
abc
Loading... done
Line 1 joins its three values with - instead of a space. Line 2 uses the empty string, so nothing goes between them. Lines 3 and 4 use end="", so print does not move to a new line, and the next print carries on where it stopped. Line 5 ends normally, finishing the line.
Key idea
A keyword argument (name=value in a call) is matched by parameter name, so order does not matter and it can skip
over defaults. In a call, positional arguments come first, then keyword arguments.
Watch out
Breaking the order rule in a call. Positional arguments must come before keyword arguments, for the same reason as in 10.1: Python fills positions from the left, so it must see them all first.
def order(drink, size="medium", milk="whole"):
"""Return a description of a coffee order."""
return f"A {size} {drink} with {milk} milk"
print(order(size="large", "latte"))SyntaxError: positional argument follows keyword argumentFix: order("latte", size="large"). A related mistake is giving a parameter a value twice, once by position and once by name:
def order(drink, size="medium", milk="whole"):
"""Return a description of a coffee order."""
return f"A {size} {drink} with {milk} milk"
print(order("latte", drink="tea"))TypeError: order() got multiple values for argument 'drink'"latte" already went to drink by position, so drink="tea" tries to fill it again. Each parameter gets exactly one value. (A misspelled name, such as milks="oat", gives got an unexpected keyword argument 'milks'.)
Pause and try
Question 2 · Multiple choice
What does this program print?
def describe(animal, sound="no sound", legs=4):
"""Return a sentence about an animal."""
return f"The {animal} has {legs} legs and makes {sound}."
print(describe("bird", legs=2))The bird has 2 legs and makes no sound.The bird has 4 legs and makes 2.The bird has 4 legs and makes no sound.- An error, because
soundwas skipped
Check your reasoning
A. "bird" goes to animal by position. legs=2 is a keyword argument, so it goes to legs by name. sound gets nothing, so it uses its default, "no sound".
- B is what
describe("bird", 2)prints: without the keyword, the 2 would go by position to the next parameter,sound. - C ignores the keyword argument. The default 4 is used only when
legsis missing, and here it was passed. - D is wrong: skipping a parameter that has a default is exactly what keyword arguments allow.
Reread 10.2 Keyword Arguments if you chose differently.
10.3 The Mutable Default Argument Trap
Defaults look simple, but one kind of default hides a famous bug. Imagine a shopping cart function: add an item to a cart and return the cart, and if no cart is given, start an empty one. An empty list looks like the obvious default.
Before you see the bug, recall Day 6: b = a does not copy a list. It makes a second name for the same list, so a change made through one name shows through the other. Keep that in mind.
Worked example: A default that remembers
def add_item(item, cart=[]):
"""Add item to cart and return the cart."""
cart.append(item)
return cart
first = add_item("apple")
print(first)
second = add_item("bread")
print(second)
print(first is second)['apple']
['apple', 'bread']
TrueThe second call should have returned ['bread']: a new cart with one item. Instead bread was added to the first cart, and line 11 confirms with is (Day 4) that first and second are the very same list.
Here is why. A default value is worked out once, when Python runs the def line, and is stored with the function. It is not made fresh on each call. So there is exactly one default list, and every call that leaves out cart gets that same list:
| Step | The one default list | first |
second |
|---|---|---|---|
def runs |
[] |
||
add_item("apple") |
['apple'] |
that list | |
add_item("bread") |
['apple', 'bread'] |
that list | that list |
The cart.append(item) on line 3 changes that list in place, so the change stays for the next call. This is Day 6’s aliasing, hidden inside a function.
The fix is to use a default that cannot be changed, None (“no cart given”), and make the new list inside the function, where the code runs on every call:
def add_item(item, cart=None):
"""Add item to cart and return the cart. With no cart, start a new one."""
if cart is None:
cart = []
cart.append(item)
return cart
first = add_item("apple")
second = add_item("bread")
print(first)
print(second)
print(first is second)
groceries = ["eggs"]
add_item("milk", groceries)
print(groceries)['apple']
['bread']
False
['eggs', 'milk']
Lines 3 and 4 run on every call. When cart is None, line 4 makes a brand-new empty list for this call only, so first and second are now different lists. The last three lines show that passing your own list still works: "milk" is added to groceries itself.
Why are "Hello", 2, and 12.5 safe defaults when a list is not? Strings and numbers are immutable: nothing can change them in place, so sharing one is harmless. Lists, dictionaries, and sets are mutable: they can change in place, so a shared one carries changes from call to call. A mutable default argument is a default value that is a list, dictionary, or set.
Key idea
A default is made once, when def runs, and shared by every call. Never use a list, dictionary, or set as a default.
Use None and create the new value inside the function: if cart is None: cart = [].
Watch out
Believing the bug is in the caller. When second shows ['apple', 'bread'], it is tempting to look for code that added apple to second. There is none. The extra item comes from the shared default. Whenever a function “remembers” things between calls that it should not, look at its def line for =[], ={}, or =set().
Pause and try
Question 3 · Tracing
The same trap, with a dictionary. What does this program print? Track the one shared default dictionary.
def tally(word, counts={}):
"""Count one more use of word and return the counts."""
counts[word] = counts.get(word, 0) + 1
return counts
print(tally("yes"))
print(tally("no"))
print(tally("yes", {}))
print(tally("yes"))Check your reasoning
{'yes': 1}
{'yes': 1, 'no': 1}
{'yes': 1}
{'yes': 2, 'no': 1}| Call | Which dictionary | After the call |
|---|---|---|
tally("yes") |
The shared default | {'yes': 1} |
tally("no") |
The shared default | {'yes': 1, 'no': 1} |
tally("yes", {}) |
A new one from the call | {'yes': 1} |
tally("yes") |
The shared default again | {'yes': 2, 'no': 1} |
The third call passes its own empty dictionary, {}, which is made fresh when that call runs, so it starts from nothing. The fourth call goes back to the shared default, which still holds the counts from the first two calls. Line 3 uses get with a default of 0 (Day 8), so a new word starts at 0 before adding 1.
Reread 10.3 The Mutable Default Argument Trap if your output differed.
10.4 Any Number of Arguments: *args
Day 9 asked you to write a function that takes a list of numbers. That works, but the caller has to build a list first. max(3, 9, 4) does not need a list: it takes as many numbers as you give it. How can your own function do that, when you cannot know how many parameters to write?
Recall Day 7’s starred unpacking: in first, *rest = items, the star means “collect everything left over”. A star in front of a parameter means the same thing for a call. A parameter written as *args collects all the leftover positional arguments into a tuple. The name after the star can be anything (*scores, *prices); args, short for arguments, is the name programmers use when there is no better one.
Worked example: An average of any length
def average(*scores):
"""Return the average of any number of scores."""
print("scores is", scores)
return sum(scores) / len(scores)
print(average(85, 90, 78))
print(average(100, 60))
print(average(72))scores is (85, 90, 78)
84.33333333333333
scores is (100, 60)
80.0
scores is (72,)
72.0Line 3 prints scores so you can see what Python built:
- Three arguments arrive as the tuple
(85, 90, 78). - Two arguments arrive as
(100, 60). - One argument still arrives as a tuple,
(72,). The trailing comma is Day 7’s single-item tuple.
Because scores is always a tuple, line 4 can use sum and len on it whatever the number of arguments.
A *args parameter can come after ordinary parameters. The ordinary ones are filled first, from the left, and the star collects whatever is left, which may be nothing at all:
def report(title, *scores):
"""Print a title and a count, then each score on its own line."""
print(f"{title}: {len(scores)} scores")
for score in scores:
print("-", score)
report("Quiz 1", 8, 10, 7)
report("Quiz 2")Quiz 1: 3 scores
- 8
- 10
- 7
Quiz 2: 0 scores
"Quiz 1" fills title, and the three numbers go into scores. report("Quiz 2") fills title and leaves nothing over, so scores is the empty tuple (): len gives 0 and the for loop runs zero times. An empty *args tuple can catch you out, though. Call average (without its print line) with no arguments, and it divides by len(()), which is 0:
def average(*scores):
"""Return the average of any number of scores."""
return sum(scores) / len(scores)
print(average())ZeroDivisionError: division by zero
This is Day 6’s empty-list average again. A real version should check the length first, as biggest_gap does in the question below.
The star works the other way round in a call. If your numbers are already in a list, * in front of the list spreads its items out as separate positional arguments. This is called unpacking the list into the call:
def average(*scores):
"""Return the average of any number of scores."""
return sum(scores) / len(scores)
marks = [70, 80, 90]
print(average(*marks))
print(*marks)
print(marks)80.0
70 80 90
[70, 80, 90]
average(*marks) is the same as average(70, 80, 90). print(*marks) is the same as print(70, 80, 90), which prints the three values separated by spaces, while print(marks) prints one value, the list itself.
Key idea
A *args parameter collects any number of extra positional arguments into a tuple, which may be empty. In a call,
*items does the reverse: it spreads a list out into separate arguments.
Watch out
Passing a list where separate values are expected. The list goes into the tuple as one item:
def average(*scores):
"""Return the average of any number of scores."""
return sum(scores) / len(scores)
marks = [70, 80, 90]
print(average(marks))TypeError: unsupported operand type(s) for +: 'int' and 'list'Inside average, scores is ([70, 80, 90],): a tuple holding one list. sum starts at 0 and tries 0 + [70, 80, 90], and Python cannot add a number and a list. Either pass the numbers separately, average(70, 80, 90), or unpack the list, average(*marks).
Pause and try
Question 4 · Tracing
What does this program print? The last call is the tricky one.
def biggest_gap(label, *values):
"""Return the label and the gap between the largest and smallest value."""
if len(values) < 2:
return f"{label}: need two values"
return f"{label}: {max(values) - min(values)}"
print(biggest_gap("Temps", 12, 19, 7, 15))
print(biggest_gap("Single", 4))
print(biggest_gap(3, 9))Check your reasoning
Temps: 12
Single: need two values
3: need two values| Call | label |
values |
Result |
|---|---|---|---|
biggest_gap("Temps", 12, 19, 7, 15) |
"Temps" |
(12, 19, 7, 15) |
19 - 7 is 12 |
biggest_gap("Single", 4) |
"Single" |
(4,) |
Fewer than 2 values |
biggest_gap(3, 9) |
3 |
(9,) |
Fewer than 2 values |
The ordinary parameter label is always filled first. In the last call that takes the 3, even though it was meant as a value, and only 9 is left for values. An f-string turns the 3 into text, so there is no error, just a surprising label. max and min work on a tuple just as they do on a list.
Reread 10.4 Any Number of Arguments: *args if your output differed.
10.5 Keyword Arguments by the Handful: **kwargs
*args collects extra positional arguments. Sometimes you want extra keyword arguments instead: a profile function where one person has an age and a city, and another has a club and a favorite color, and you cannot list every possible detail as a parameter.
A parameter written as **kwargs (two stars) collects all the leftover keyword arguments into a dictionary (Day 8). Each keyword becomes a key, as a string, and the value after = becomes its value. kwargs is short for keyword arguments; again, any name works.
Worked example: A profile with any details
def make_profile(name, **details):
"""Print a profile: the name, then each detail given by keyword."""
print("details is", details)
print(f"Profile: {name}")
for field, value in details.items():
print(f" {field}: {value}")
make_profile("Ana", age=15, city="Lima", club="Chess")
make_profile("Ben")details is {'age': 15, 'city': 'Lima', 'club': 'Chess'}
Profile: Ana
age: 15
city: Lima
club: Chess
details is {}
Profile: Ben"Ana"fillsnameby position.age=15,city="Lima", andclub="Chess"match no parameter by name, so all three go into the dictionarydetails, in the order they were typed. The keys are the strings'age','city', and'club'.- Lines 5 and 6 loop over the dictionary with
items()from Day 8. make_profile("Ben")passes no keyword arguments, sodetailsis the empty dictionary and the loop runs zero times.
Two stars also work in a call: ** in front of a dictionary unpacks it into keyword arguments, one per key. This is handy when the settings are stored in a dictionary:
def order(drink, size="medium", milk="whole"):
"""Return a description of a coffee order."""
return f"A {size} {drink} with {milk} milk"
usual = {"size": "large", "milk": "oat"}
print(order("latte", **usual))
print(order("mocha", **usual))A large latte with oat milk
A large mocha with oat milk
order("latte", **usual) is the same as order("latte", size="large", milk="oat"). Each key must be the name of a parameter.
Keyword-only parameters
A *args parameter swallows every leftover positional argument. So what about a parameter written after it? It can never be filled by position: the star got there first. It can only be set by keyword. A parameter that can only be given by keyword is a keyword-only parameter. That sounds like a limitation, but it is often exactly what you want:
def total_price(*prices, discount=0):
"""Add up any number of prices, then take off a discount (0.1 is 10%)."""
subtotal = sum(prices)
return subtotal - subtotal * discount
print(total_price(20, 30, 50))
print(total_price(20, 30, 50, discount=0.1))
print(total_price(20, 30, 50, 0.1))100
90.0
100.1
- The first call has no discount, so
discountis its default 0, and the total is 100. - The second sets
discount=0.1by keyword: 10% off 100 is 90.0. - The third shows why the rule is useful. The caller meant 0.1 as the discount, but it was positional, so it went into
pricesas a fourth price: 100.1. Becausediscountis keyword-only, a reader of the calltotal_price(20, 30, 50, discount=0.1)can see at once which number is the discount.
All the kinds of parameter can appear in one def, but only in this order: ordinary parameters, then ones with defaults, then *args, then keyword-only ones, then **kwargs last. This program uses all five, with short names because only the mechanics matter here:
def show(a, b=2, *args, c=3, **kwargs):
print(a, b, args, c, kwargs)
show(1)
show(1, 5, 6, 7, c=8, d=9)1 2 () 3 {}
1 5 (6, 7) 8 {'d': 9}
| Parameter | show(1) |
show(1, 5, 6, 7, c=8, d=9) |
|---|---|---|
a |
1 | 1 |
b=2 |
2 (default) | 5 |
*args |
() |
(6, 7) |
c=3 |
3 (default) | 8 (by keyword only) |
**kwargs |
{} |
{'d': 9} |
Python fills positions from the left (a, then b), the star takes the rest (6 and 7), keywords that match a parameter go there (c=8), and keywords that match nothing go into the dictionary (d=9).
Key idea
**kwargs collects extra keyword arguments into a dictionary of name to value, and **settings in a call spreads a
dictionary out as keyword arguments. Parameters after *args are keyword-only.
Watch out
Passing a positional argument where only keywords are collected. Two stars collect keyword arguments only:
def make_profile(name, **details):
"""Print a profile: the name, then each detail given by keyword."""
print(f"Profile: {name}")
for field, value in details.items():
print(f" {field}: {value}")
make_profile("Ana", "Lima")TypeError: make_profile() takes 1 positional argument but 2 were given"Lima" has no name, so it cannot be a dictionary entry, and there is no *args to catch it. Give it a name: make_profile("Ana", city="Lima").
Pause and try
Question 5 · Tracing
What does this program print? Decide where each argument goes before you look.
def describe_pizza(size, *toppings, crust="thin", **extras):
print(f"{size} pizza, {crust} crust")
print("Toppings:", toppings)
print("Extras:", extras)
describe_pizza("Large", "olives", "peppers", sauce="spicy", crust="thick")Check your reasoning
Large pizza, thick crust
Toppings: ('olives', 'peppers')
Extras: {'sauce': 'spicy'}| Argument | Goes to | Why |
|---|---|---|
"Large" |
size |
First positional argument |
"olives", "peppers" |
toppings |
Leftover positional arguments, as a tuple |
sauce="spicy" |
extras |
Keyword that matches no parameter |
crust="thick" |
crust |
Keyword that matches a parameter |
crust is keyword-only, because it comes after *toppings. The keyword crust="thick" matches it by name, so it does not go into extras, even though it was written last.
Reread 10.5 Keyword Arguments by the Handful: **kwargs if your output differed.
10.6 Functions Are Values
Here is a new kind of problem. You have an announce function that prints a message. Sometimes the message should be shouted, sometimes whispered, and next week someone will want a third style. You could add a style parameter holding a string and a long if chain inside announce, and edit that chain every time a style is added. It would be better if the caller could hand over the styling itself: a piece of behavior, not a piece of data.
Python allows this because a function is a value, like a number or a list. def shout(...) does two things: it builds a function object, and it stores it in the name shout, just as count = 0 stores 0 in count. The name without parentheses refers to the function itself. Adding parentheses calls it.
>>> def shout(text):
... return text.upper() + "!"
...
>>> type(shout)
<class 'function'>
>>> yell = shout
>>> yell("fire drill")
'FIRE DRILL!'
>>> shout("fire drill")
'FIRE DRILL!'
>>> len
<built-in function len>
>>> measure = len
>>> measure("hello")
5type(shout)shows the function is a value of typefunction.yell = shouthas no parentheses, so nothing is called. It makesyella second name for the same function, exactly likeb = awith a list.yell("fire drill")andshout("fire drill")call the same code.- Built-in functions are values too.
lenwithout parentheses is the function itself, andmeasure = lengives it a second name.
Because a function is a value, you can pass it as an argument. The parameter then holds a function, and the code inside can call it with parentheses:
Worked example: Passing the style in
def shout(text):
return text.upper() + "!"
def whisper(text):
return text.lower() + "..."
def announce(message, style):
"""Print message after passing it through the style function."""
print(style(message))
announce("Class Starts Now", shout)
announce("Class Starts Now", whisper)CLASS STARTS NOW!
class starts now...Follow the first call, announce("Class Starts Now", shout):
messagegets the string"Class Starts Now", andstylegets the functionshout. Notice that the call writesshout, with no parentheses: the function is handed over, not run.- Line 11 works out
style(message).stylerefers toshout, so this callsshout("Class Starts Now"), which returns"CLASS STARTS NOW!". printshows that string.
The second call is identical except that style refers to whisper. announce never changes; the behavior that was passed in does.
A function that takes another function as an argument (or returns one) is called a higher-order function. Here is a second one that varies something different: the passed-in function returns True or False, and the higher-order function uses it to decide what to count. This is the counter pattern from Day 5 with the test left blank for the caller to fill in:
def count_if(items, test):
"""Return how many items make test(item) True."""
count = 0
for item in items:
if test(item):
count += 1
return count
def is_even(number):
return number % 2 == 0
def is_big(number):
return number > 50
numbers = [12, 75, 33, 90, 48]
print(count_if(numbers, is_even))
print(count_if(numbers, is_big))3
2
Inside count_if, line 5 calls test(item) for each number. With is_even, that is True for 12, 90, and 48, so the count is 3. With is_big, it is True for 75 and 90, so the count is 2. One loop, written once, counts anything you can write a test for.
Key idea
A function’s name without parentheses is the function itself, a value you can store or pass. With parentheses, it is a call. To pass a function to another function, write its name without parentheses.
Watch out
Calling the function instead of passing it. Parentheses after the name run the function right away:
def shout(text):
return text.upper() + "!"
def announce(message, style):
"""Print message after passing it through the style function."""
print(style(message))
announce("Class Starts Now", shout())TypeError: shout() missing 1 required positional argument: 'text'shout() tries to call shout immediately, with no argument, before announce even starts, and shout needs a text. Even with an argument, shout("hi") would hand announce the string "HI!" rather than the function, and style(message) would then fail with 'str' object is not callable (“callable” means “can be called with parentheses”). Write announce("Class Starts Now", shout).
Pause and try
Question 6 · Multiple choice
Using shout and announce from announce.py, which call prints GO TEAM!?
announce("Go team", shout())announce("Go team", shout)announce("Go team", shout("Go team"))announce(shout, "Go team")
Check your reasoning
B. shout without parentheses passes the function. announce stores it in style and calls style("Go team"), which returns "GO TEAM!".
- A calls
shoutwith no argument beforeannounceruns:TypeError: shout() missing 1 required positional argument: 'text'. - C calls
shoutfirst and passes its result, the string"GO TEAM!". Insideannounce,style(message)tries to call a string:TypeError: 'str' object is not callable. - D has the arguments in the wrong order:
messageis the function andstyleis the string"Go team", sostyle(message)again tries to call a string and gives the sameTypeError.
Reread 10.6 Functions Are Values if you chose differently.
10.7 lambda Expressions
count_if is useful, but every test needs its own def: is_even, is_big, and next is_between_30_and_60. Each is one line of real work, used once, with two blank lines around it. For tiny one-use functions, that is a lot of ceremony.
A lambda expression builds a small function right where you need it, without a def and without a name:
lambda parameters: expression
lambdais a keyword. The parameters come next, separated by commas, then a colon.- After the colon is one expression. When the function is called, Python works out that expression and returns its value automatically. There is no
returnkeyword. - The whole thing is an expression whose value is a function, so it can go anywhere a value can: as an argument, in a list, after
=.
lambda n: n * 2 means the same as:
def double(n):
return n * 2
except that the lambda version has no name of its own.
>>> double = lambda n: n * 2
>>> double(21)
42
>>> type(double)
<class 'function'>
>>> (lambda price, quantity: price * quantity)(2.5, 4)
10.0The first line stores a lambda in a name so you can try it: double(21) is 42, and type shows it is an ordinary function. The last line builds a two-parameter lambda and calls it at once with (2.5, 4), giving 10.0. Both are for seeing how lambdas work. In real code, PEP 8 says: if you want a name, use def. The place where lambdas belong is as an argument:
Worked example: Tests written in place
def count_if(items, test):
"""Return how many items make test(item) True."""
count = 0
for item in items:
if test(item):
count += 1
return count
numbers = [12, 75, 33, 90, 48]
print(count_if(numbers, lambda n: n % 2 == 0))
print(count_if(numbers, lambda n: n > 50))
print(count_if(numbers, lambda n: 30 <= n <= 60))3
2
2Each call writes its test right where it is used. lambda n: n % 2 == 0 is is_even without the def; lambda n: n > 50 is is_big. The third test is new, and it took one line rather than four: a chained comparison from Day 4, True for 33 and 48, so the count is 2. count_if itself is the same function as in 10.6, and it cannot tell whether it was given a def function or a lambda.
The expression after the colon can be any expression, including a conditional expression from Day 4:
def announce(message, style):
"""Print message after passing it through the style function."""
print(style(message))
announce("Lunch is served", lambda text: text[:8] + "...")
announce("Lunch is served", lambda text: f"** {text} **")
announce("Hi all", lambda text: text if len(text) <= 8 else text[:8] + "...")Lunch is...
** Lunch is served **
Hi all
The first lambda keeps the first 8 characters and adds dots. The second wraps the text in stars with an f-string. The third chooses: if the text is at most 8 characters, return it unchanged; otherwise shorten it. "Hi all" has 6 characters, so it comes back as it was.
Key idea
lambda x: expression is a small, unnamed function that returns the value of one expression. Use it for short
functions you pass to another function; use def for anything you name or reuse.
Watch out
Writing return (or any statement) in a lambda. The part after the colon must be an expression, and return is a statement:
def count_if(items, test):
"""Return how many items make test(item) True."""
count = 0
for item in items:
if test(item):
count += 1
return count
print(count_if([12, 75, 33], lambda n: return n > 50))SyntaxError: invalid syntaxRemove return: lambda n: n > 50. The lambda returns the expression’s value without being told. If you find yourself wanting an if statement, a loop, or several lines inside a lambda, write a def function instead.
Pause and try
Question 7 · Tracing
What does this program print? Work out what each lambda does to one word first.
def apply_to_each(words, change):
"""Return a new list holding change(word) for each word."""
result = []
for word in words:
result.append(change(word))
return result
words = ["tea", "coffee", "juice"]
print(apply_to_each(words, lambda w: w[0].upper() + w[1:]))
print(apply_to_each(words, lambda w: len(w)))
print(apply_to_each(words, lambda w: w if len(w) > 3 else w * 2))Check your reasoning
['Tea', 'Coffee', 'Juice']
[3, 6, 5]
['teatea', 'coffee', 'juice']- The first lambda makes the first character a capital (
w[0].upper()) and joins it to the rest (w[1:], Day 3 slicing). - The second returns each word’s length, so the new list holds numbers, not strings.
- The third uses a conditional expression: only
"tea", at 3 characters, is not longer than 3, so it is repeated withw * 2; the other two come back unchanged.
apply_to_each builds a new list each time, so words itself never changes between the calls.
Reread 10.7 lambda Expressions if your output differed.
10.8 Sorting with key
Day 6 sorted lists with sorted(), which returns a new sorted list, and sort(), which sorts a list in place. Both put items in their natural order: numbers from smallest to largest, and strings alphabetically, with capitals before small letters (Day 4). But often you want a different order: words by length, names ignoring capitals, students by score. You need a way to tell sorted what to compare.
Both have an optional keyword argument key, which takes a function. Python calls that function once on each item and sorts the items by the values it returns. Those returned values are called the sort keys. The items themselves are not changed: only their order is.
Worked example: Four ways to sort the same words
>>> words = ["pear", "Fig", "banana", "kiwi", "apple"]
>>> sorted(words)
['Fig', 'apple', 'banana', 'kiwi', 'pear']
>>> sorted(words, key=len)
['Fig', 'pear', 'kiwi', 'apple', 'banana']
>>> sorted(words, key=str.lower)
['apple', 'banana', 'Fig', 'kiwi', 'pear']
>>> sorted(words, key=len, reverse=True)
['banana', 'apple', 'pear', 'kiwi', 'Fig']
>>> words
['pear', 'Fig', 'banana', 'kiwi', 'apple']
>>> words.sort(key=len)
>>> words
['Fig', 'pear', 'kiwi', 'apple', 'banana']sorted(words) uses natural order. 'Fig' comes first because a capital F comes before every small letter.
key=len passes the built-in len function (no parentheses: 10.6). Python works out a key for each word and sorts by those numbers:
| Item | Key: len(item) |
|---|---|
"pear" |
4 |
"Fig" |
3 |
"banana" |
6 |
"kiwi" |
4 |
"apple" |
5 |
Sorting by key gives 3, 4, 4, 5, 6, so the result is Fig, pear, kiwi, apple, banana.
key=str.lower sorts by each word’s lowercase version, so capitals no longer jump ahead. str.lower is the lower method from Day 3, named through its type, str, without parentheses, so it is the function itself; Python calls it on each word. The words in the result keep their original capitals: 'Fig', not 'fig'.
reverse=True, the keyword argument from Day 6, still sorts from largest to smallest, now by key.
Asking for words after the four calls shows it unchanged, because sorted makes a new list each time. The last two lines use the list method sort, which takes the same key and reverse keyword arguments but sorts the list in place and returns nothing (the REPL shows no result line).
Look again at pear and kiwi. Both have length 4, a tie. In the result, pear is before kiwi, just as it was in the original list. That is a guarantee, not luck: Python’s sort is stable, meaning items with equal keys keep their original order. Even with reverse=True, the tied pear and kiwi stay in their original order.
The key function can be a lambda, which is how you sort a list of dictionaries (Day 8) by one field:
students = [
{"name": "Maya", "grade": 10, "score": 88},
{"name": "Leo", "grade": 9, "score": 95},
{"name": "Ava", "grade": 10, "score": 95},
{"name": "Sam", "grade": 9, "score": 72},
]
by_score = sorted(students, key=lambda student: student["score"], reverse=True)
for student in by_score:
print(student["name"], student["score"])Leo 95
Ava 95
Maya 88
Sam 72
For each student dictionary, the lambda returns student["score"], so the list is sorted by score, highest first because of reverse=True. Leo and Ava tie on 95, and Leo was earlier in the original list, so stability keeps him first.
Sorting by one field, then another
What if ties should be broken by a second field, say by grade and then, within each grade, by name? A key can be a tuple (Day 7), and Python compares tuples item by item: first items first, and only if those are equal, the second items.
>>> (9, "Sam") < (10, "Ava")
True
>>> (10, "Ava") < (10, "Maya")
True
>>> (9, "Leo") < (9, "Sam")
True(9, "Sam") is less than (10, "Ava") because 9 is less than 10; the names are never looked at. (10, "Ava") and (10, "Maya") tie on 10, so the names decide. So a key that returns (grade, name) sorts by grade, and by name within a grade:
students = [
{"name": "Maya", "grade": 10, "score": 88},
{"name": "Leo", "grade": 9, "score": 95},
{"name": "Ava", "grade": 10, "score": 95},
{"name": "Sam", "grade": 9, "score": 72},
]
by_grade_then_name = sorted(
students, key=lambda student: (student["grade"], student["name"])
)
for student in by_grade_then_name:
print(student["grade"], student["name"])9 Leo
9 Sam
10 Ava
10 Maya
Key idea
sorted(items, key=function) calls the function on each item and sorts by what it returns, leaving the items
themselves unchanged. Add reverse=True for largest first. Ties keep their original order, and a tuple key breaks
ties with a second field.
Watch out
Calling the key function. The key must be a function, so it gets no parentheses:
words = ["pear", "fig", "banana"]
print(sorted(words, key=len()))TypeError: len() takes exactly one argument (0 given)len() runs at once, with nothing to measure, before sorted starts. Write key=len, and let sorted call it on each item. The same goes for key=str.lower, not key=str.lower().
Pause and try
Question 8 · Tracing
What does this program print? Each item is a tuple of a name and a score.
scores = [("Ben", 95), ("Ana", 82), ("Dee", 70), ("Cal", 82)]
print(sorted(scores))
print(sorted(scores, key=lambda pair: pair[1]))
print(sorted(scores, key=lambda pair: pair[1], reverse=True))Check your reasoning
[('Ana', 82), ('Ben', 95), ('Cal', 82), ('Dee', 70)]
[('Dee', 70), ('Ana', 82), ('Cal', 82), ('Ben', 95)]
[('Ben', 95), ('Ana', 82), ('Cal', 82), ('Dee', 70)]- Line 2 has no key, so the tuples are compared as whole tuples: by their first items, the names, alphabetically.
- Line 3’s key returns each score, so the list is sorted 70, 82, 82, 95. Ana and Cal tie on 82; Ana was before Cal in the original list, so stability keeps her first.
- Line 4 sorts by score from largest to smallest. The tie is still kept in its original order: Ana before Cal.
Reread 10.8 Sorting with key if your output differed.
10.9 min() and max() with key
Day 6 used min and max to find the smallest and largest number in a list. Like sorted, they compare items in their natural order, and like sorted, they accept a key function to compare something else. With a key, max returns the item whose key is largest, not the key itself.
>>> words = ["pear", "banana", "fig", "cherry"]
>>> max(words)
'pear'
>>> max(words, key=len)
'banana'
>>> min(words, key=len)
'fig'
>>> len(max(words, key=len))
6max(words)compares alphabetically:'pear'comes last.max(words, key=len)compares lengths.bananaandcherryboth have 6 letters. When there is a tie,maxandminreturn the first one, so the result is'banana'.min(words, key=len)gives the shortest word,'fig'.maxreturns the word, not its length. If you want the length, measure the word it returns:len(max(words, key=len))is 6.
Worked example: The cheapest product
products = [
{"name": "pen", "price": 1.5},
{"name": "notebook", "price": 3.25},
{"name": "eraser", "price": 0.75},
]
cheapest = min(products, key=lambda product: product["price"])
print(cheapest)
print(f"Cheapest: {cheapest['name']} at ${cheapest['price']:.2f}")
prices = {"pen": 1.5, "notebook": 3.25, "eraser": 0.75}
print(max(prices))
print(max(prices, key=prices.get)){'name': 'eraser', 'price': 0.75}
Cheapest: eraser at $0.75
pen
notebookmin(products, key=lambda product: product["price"])compares the three products by price and returns the whole cheapest dictionary, so line 8 can show both its name and its price.- Line 8 uses single quotes inside the f-string’s braces, because the f-string itself uses double quotes.
pricesis a dictionary. Looping over a dictionary goes through its keys (Day 8), somax(prices)compares the names:'pen'is last alphabetically.max(prices, key=prices.get)uses the dictionary’s owngetmethod as the key function, without parentheses. For each name, Python callsprices.get(name), which gives its price, so the result is the name with the highest price:'notebook'.
Key idea
max(items, key=function) and min(items, key=function) return the item whose key is largest or smallest; on a tie,
the first one. For a dictionary, max(d, key=d.get) gives the key with the largest value.
Watch out
max or min on an empty list. As on Day 6, with nothing to compare there is no answer:
scores = []
best = max(scores)
print("Best score:", best)ValueError: max() iterable argument is emptyDay 6 read “iterable” in this message as “something you can loop over”. It is a word you will meet often from now on: an iterable is anything a for loop can go through, such as a list, a string, or a dictionary. If the list might be empty, give the keyword argument default, the value to return in that case:
scores = []
best = max(scores, default=None)
print("Best score:", best)Best score: NonePause and try
Question 9 · Tracing
What does this program print?
words = ["kiwi", "plum", "fig", "lime", "pear"]
print(max(words, key=len))
print(min(words, key=len))
print(max(words, key=lambda word: word[-1]))Check your reasoning
kiwi
fig
pear| Word | len |
Last letter |
|---|---|---|
"kiwi" |
4 | i |
"plum" |
4 | m |
"fig" |
3 | g |
"lime" |
4 | e |
"pear" |
4 | r |
- Four words tie for the longest, at 4 letters.
maxreturns the first,kiwi. figis the only 3-letter word.- The last lambda compares last letters as strings.
rcomes latest in the alphabet, so the answer ispear, the word, not the letter.
Reread 10.9 min() and max() with key if your output differed.
10.10 Reading a Function Signature
You now know every kind of parameter, which means you can read the documentation for any function. The first line of a function’s definition, its name and parameter list, is its signature. It tells you everything about how to call it: which arguments are required, which are optional and what their defaults are, and which must be given by position or by keyword. The Python documentation shows a signature at the top of each function’s entry. Here are some you have used:
round(number, ndigits=None)
enumerate(iterable, start=0)
print(*objects, sep=' ', end='\n', file=None, flush=False)
sorted(iterable, /, *, key=None, reverse=False)
max(iterable, /, *, default, key=None)
Most of this you can already read. round has a required number and an optional ndigits. enumerate has an optional start that defaults to 0, which is how enumerate(word, 1) worked on Day 5. print has *objects, so it takes any number of values, followed by keyword-only parameters with defaults: sep is a space and end is a newline, exactly as you saw in 10.2. Two markers are new:
- A bare
*with no name after it means: every parameter after this point is keyword-only. It works like*argswithout collecting anything. Insorted,keyandreversecome after the*, so they must be given by keyword. - A
/means: every parameter before this point is positional-only, so it can never be given by keyword. Insorted,iterableis before the/, so you writesorted(words), neversorted(iterable=words).
You can use both markers in your own functions:
Worked example: Splitting a bill
def split_bill(total, /, people=2, *, tip=0.15):
"""Return each person's share of total, with a tip added."""
return round(total * (1 + tip) / people, 2)
print(split_bill(60))
print(split_bill(60, 3))
print(split_bill(60, people=4, tip=0.2))34.5
23.0
18.0Read the signature split_bill(total, /, people=2, *, tip=0.15) from left to right:
| Parameter | Kind | Default |
|---|---|---|
total |
Required, positional-only (before /) |
none |
people |
Optional, by position or by keyword (between / and *) |
2 |
tip |
Optional, keyword-only (after *) |
0.15 |
split_bill(60)uses both defaults: 60 plus a 15% tip is 69, split 2 ways is 34.5.split_bill(60, 3)setspeopleby position: 69 split 3 ways is 23.0.split_bill(60, people=4, tip=0.2)sets both by keyword: 60 plus 20% is 72, split 4 ways is 18.0.
The round(..., 2) hides the tiny float errors from Day 2 that a tip like 0.15 can cause.
The markers are enforced. Break them, and Python says which rule you broke:
def split_bill(total, /, people=2, *, tip=0.15):
"""Return each person's share of total, with a tip added."""
return round(total * (1 + tip) / people, 2)
print(split_bill(total=60))TypeError: split_bill() got some positional-only arguments passed as keyword arguments: 'total'
def split_bill(total, /, people=2, *, tip=0.15):
"""Return each person's share of total, with a tip added."""
return round(total * (1 + tip) / people, 2)
print(split_bill(60, 3, 0.2))TypeError: split_bill() takes from 1 to 2 positional arguments but 3 were given
The second message says “1 to 2” because only total and people can be filled by position. The third value, meant as the tip, had nowhere to go. The same rules apply to the built-in functions:
>>> sorted([3, 1, 2], reverse=True)
[3, 2, 1]
>>> sorted([3, 1, 2], None, True)
Traceback (most recent call last):
...
TypeError: sorted expected 1 argument, got 3
>>> "a,b,c".split(",", maxsplit=1)
['a', 'b,c']
>>> round(3.14159, ndigits=2)
3.14
>>> len(obj="abc")
Traceback (most recent call last):
...
TypeError: len() takes no keyword argumentssorted([3, 1, 2], reverse=True)follows the signature: the list by position,reverseby keyword.sorted([3, 1, 2], None, True)tries to givekeyandreverseby position. They are after the*, sosortedaccepts only 1 positional argument.str.split(sep=None, maxsplit=-1)has no markers, somaxsplit=1can be given by keyword. It stops after one split.round(number, ndigits=None)also has no markers, sondigits=2works by keyword.lentakes its one argument by position only, solen(obj="abc")is an error.
Why do these markers exist? Positional-only parameters let the function’s author rename them later without breaking anyone’s code, since no call can use the name. Keyword-only parameters protect calls like total_price from 10.5, where a value in the wrong place would be silently misused, and they make calls such as sorted(scores, reverse=True) say what they mean.
Older entries in the documentation sometimes use square brackets for optional arguments, as in range(start, stop[, step]): the part in brackets may be left out. The brackets are notation for the reader, not Python you type.
Key idea
A signature lists a function’s parameters in order. Names with = have defaults; *name collects extra positional
arguments and **name extra keyword arguments; parameters before / are positional-only, and parameters after *
are keyword-only.
Watch out
Typing the markers or defaults into a call. The signature describes the call; it is not the call. sorted(words, /, *, key=len) is a syntax error. Read the signature, then write only the arguments you need: sorted(words, key=len). Leave out every optional parameter whose default you are happy with.
Pause and try
Question 10 · Multiple choice
A library’s documentation shows this signature:
resize(image, /, width, height=None, *, keep_ratio=True)Which call is an error?
resize("cat.png", 300)resize("cat.png", width=300, keep_ratio=False)resize("cat.png", 300, 200)resize("cat.png", 300, 200, False)
Check your reasoning
D. keep_ratio is after the *, so it is keyword-only. Only image, width, and height can be filled by position, so a fourth positional argument has nowhere to go:
def resize(image, /, width, height=None, *, keep_ratio=True):
"""Pretend to resize an image and describe what would happen."""
return f"{image}: width {width}, height {height}, keep ratio {keep_ratio}"
print(resize("cat.png", 300, 200, False))TypeError: resize() takes from 2 to 3 positional arguments but 4 were givenThe other three calls all work:
def resize(image, /, width, height=None, *, keep_ratio=True):
"""Pretend to resize an image and describe what would happen."""
return f"{image}: width {width}, height {height}, keep ratio {keep_ratio}"
print(resize("cat.png", 300))
print(resize("cat.png", width=300, keep_ratio=False))
print(resize("cat.png", 300, 200))cat.png: width 300, height None, keep ratio True
cat.png: width 300, height None, keep ratio False
cat.png: width 300, height 200, keep ratio True- A gives the two required arguments by position;
heightandkeep_ratiouse their defaults. - B gives
imageby position (it must be: it is before the/),widthby keyword (allowed: it is between/and*), andkeep_ratioby keyword. - C fills
image,width, andheightby position, which is allowed for all three.
The fix for D is resize("cat.png", 300, 200, keep_ratio=False).
Reread 10.10 Reading a Function Signature if you chose differently.
Putting it together
A game club keeps a leaderboard. Players are dictionaries with a name, points, and wins. The program adds players with any stats they have, ranks them by any field, and prints the top few.
def add_player(players, name, **stats):
"""Add a player to players. Points and wins start at 0 unless given."""
player = {"name": name, "points": 0, "wins": 0}
for field, value in stats.items():
player[field] = value
players.append(player)
def rank(players, *, by="points", best_first=True):
"""Return a new list of players ordered by one field."""
return sorted(players, key=lambda player: player[by], reverse=best_first)
def show(players, title="Leaderboard", limit=None):
"""Print a numbered list of players, at most limit of them if given."""
print(title)
if limit is not None:
players = players[:limit]
for place, player in enumerate(players, 1):
points = player["points"]
wins = player["wins"]
print(f"{place}. {player['name']}: {points} points, {wins} wins")
players = []
add_player(players, "Ana", points=42, wins=3)
add_player(players, "Ben", points=55, wins=2)
add_player(players, "Cal", wins=4, points=42)
add_player(players, "Dee")
show(rank(players))
show(rank(players, by="wins"), title="Most wins", limit=2)
show(rank(players, by="name", best_first=False), "A to Z")
top = max(players, key=lambda player: player["points"])
print("Top scorer:", top["name"])Leaderboard
1. Ben: 55 points, 2 wins
2. Ana: 42 points, 3 wins
3. Cal: 42 points, 4 wins
4. Dee: 0 points, 0 wins
Most wins
1. Cal: 42 points, 4 wins
2. Ana: 42 points, 3 wins
A to Z
1. Ana: 42 points, 3 wins
2. Ben: 55 points, 2 wins
3. Cal: 42 points, 4 wins
4. Dee: 0 points, 0 wins
Top scorer: Ben
Each function uses today’s ideas:
| Function or line | What it does | Idea |
|---|---|---|
add_player |
**stats collects any keyword stats; each overwrites the 0 it starts with |
10.5 |
add_player(players, "Cal", wins=4, points=42) |
Keyword order does not matter | 10.2 |
add_player(players, "Dee") |
No stats: stats is empty, so Dee keeps 0 points and 0 wins |
10.5 |
rank |
Keyword-only by and best_first with defaults; sorted with a lambda key and reverse |
10.5, 10.8 |
show |
Optional title and limit; None means “no limit given” |
10.1, 10.3 |
max(players, key=...) |
Returns the whole player dictionary with the most points | 10.9 |
Follow the three show calls:
show(rank(players))uses every default: ranked by points, best first, titleLeaderboard, no limit. Ana and Cal tie on 42; Ana was added first, so the stable sort keeps her first.show(rank(players, by="wins"), title="Most wins", limit=2)ranks by wins and shows only two rows.players[:limit](Day 6 slicing) makes a shorter list insideshow; the caller’s list is untouched.show(rank(players, by="name", best_first=False), "A to Z")sorts names alphabetically, smallest first, and passes the title by position.
Because by and best_first are keyword-only, a call reads rank(players, by="wins") rather than the puzzling rank(players, "wins", True).
Common mistakes
Today’s errors are almost all TypeErrors about how arguments match parameters. When you see one, read the end of the message:
| Message ends with | Cause | Fix |
|---|---|---|
parameter without a default follows parameter with a default |
A default before a required parameter in def |
Put required parameters first |
positional argument follows keyword argument |
f(a=1, 2) in a call |
Put positional arguments first |
got multiple values for argument 'x' |
x given by position and again by keyword |
Give each parameter one value |
got an unexpected keyword argument 'x' |
No parameter named x, and no **kwargs |
Check the spelling against the signature |
takes 2 positional arguments but 3 were given |
Too many positional arguments, or one meant for a keyword-only parameter | Use a keyword, or remove the extra value |
missing 1 required positional argument: 'x' |
A required argument left out, often from func() instead of func |
Pass the function without parentheses, or supply x |
'str' object is not callable |
Calling something that is not a function | Pass the function, not its result |
got some positional-only arguments passed as keyword arguments |
A keyword for a parameter before / |
Give it by position |
Exact wording can differ in older Python versions; the error type and the idea stay the same. The mutable default trap gives no error message at all, which is what makes it dangerous: look for =[] or ={} in a def line.
Vocabulary
| Term | Definition | Example |
|---|---|---|
| default value | A value in the def line used when a call leaves that argument out |
greeting="Hello" |
| required / optional parameter | A parameter with no default / with a default | name / greeting="Hello" |
| positional argument | An argument matched to a parameter by its place in the call | "Ana" in greet("Ana") |
| keyword argument | An argument matched by parameter name, written name=value in a call |
milk="oat" |
sep / end |
print’s keyword arguments for the text between values / after them |
print(a, b, sep="-") |
| mutable default argument | A list, dictionary, or set as a default; made once and shared by every call | def f(items=[]) |
*args |
A parameter that collects extra positional arguments into a tuple | def average(*scores) |
| unpacking (in a call) | * or ** spreading a list or dictionary out into separate arguments |
average(*marks) |
**kwargs |
A parameter that collects extra keyword arguments into a dictionary | def make_profile(name, **details) |
| keyword-only parameter | A parameter that can only be given by keyword; it comes after * or *args |
discount in total_price |
| positional-only parameter | A parameter that can only be given by position; it comes before / |
iterable in sorted |
| function object | The value a def or lambda creates; calling it runs its code |
shout without () |
| higher-order function | A function that takes a function as an argument (or returns one) | count_if(items, test) |
| lambda expression | A small unnamed function that returns the value of one expression | lambda n: n * 2 |
key |
A keyword argument of sorted, sort, min, and max: a function that gives each item’s sort key |
key=len |
| sort key | The value the key function returns for an item, used for comparing | len("pear") is 4 |
| stable sort | A sort that keeps items with equal keys in their original order | pear stays before kiwi |
| iterable | Anything a for loop can go through | a list, a string |
| signature | A function’s name and parameter list, which shows how to call it | sorted(iterable, /, *, key=None, reverse=False) |
Summary
- Default values make parameters optional; required parameters come first. Keyword arguments (
name=value) are matched by name, so they can be in any order and can skip defaults; in a call, positional arguments come first. - A default is made once, when
defruns, so a list or dictionary default is shared by every call. UseNoneand create the new value inside the function. - A
*argsparameter collects extra positional arguments into a tuple, and a**kwargsparameter collects extra keyword arguments into a dictionary; in a call, one or two stars spread a list or dictionary out. Parameters after*argsor a bare*are keyword-only; parameters before/are positional-only. - A function is a value: its name without parentheses can be stored or passed to a higher-order function, and a lambda writes a short one-expression function in place.
sorted,sort,min, andmaxtake akeyfunction and compare by what it returns;sortedandsortalso takereverse=True, and the sort is stable.
Practice
Do these in your day10 folder. Type each program yourself, run it, and only then open the answer.
Question 11 · Short answer
Defaults and keywords. Write a function with at least one default argument. Call it once using only positional arguments, and at least twice using keyword arguments, including one call that skips over a default.
Check your reasoning
One possible solution:
def describe_trip(city, days=3, transport="train"):
"""Return a one-line trip plan."""
return f"{days} days in {city}, travelling by {transport}"
print(describe_trip("Lisbon"))
print(describe_trip("Lisbon", 5))
print(describe_trip("Lisbon", 5, "plane"))
print(describe_trip(city="Rome", transport="bus"))
print(describe_trip("Oslo", transport="ferry", days=2))3 days in Lisbon, travelling by train
5 days in Lisbon, travelling by train
5 days in Lisbon, travelling by plane
3 days in Rome, travelling by bus
2 days in Oslo, travelling by ferryThe first three calls are positional: each extra argument replaces the next default from the left. describe_trip(city="Rome", transport="bus") is all keywords and skips days, which keeps its default 3; that cannot be done by position alone. The last call mixes the two: the city by position first, then two keyword arguments in an order different from the def line. Check that your def puts the required parameter before the ones with defaults.
Reread 10.1 Default Parameter Values and 10.2 Keyword Arguments if your result differed.
Question 12 · Short answer
Sorting records. Make a list of at least five dictionaries describing books (title, author, year). Print it sorted by one field, then by another. Then sort by author with ties broken by year.
Check your reasoning
One possible solution:
def show(books, heading):
"""Print a heading, then one line per book."""
print(heading)
for book in books:
print(f" {book['year']} {book['title']} ({book['author']})")
books = [
{"title": "Dune", "author": "Herbert", "year": 1965},
{"title": "Emma", "author": "Austen", "year": 1815},
{"title": "Beloved", "author": "Morrison", "year": 1987},
{"title": "Persuasion", "author": "Austen", "year": 1817},
{"title": "Kindred", "author": "Butler", "year": 1979},
]
show(sorted(books, key=lambda book: book["title"]), "By title:")
show(sorted(books, key=lambda book: book["year"]), "By year:")
by_author = sorted(books, key=lambda book: (book["author"], book["year"]))
show(by_author, "By author, then year:")By title:
1987 Beloved (Morrison)
1965 Dune (Herbert)
1815 Emma (Austen)
1979 Kindred (Butler)
1817 Persuasion (Austen)
By year:
1815 Emma (Austen)
1817 Persuasion (Austen)
1965 Dune (Herbert)
1979 Kindred (Butler)
1987 Beloved (Morrison)
By author, then year:
1815 Emma (Austen)
1817 Persuasion (Austen)
1979 Kindred (Butler)
1965 Dune (Herbert)
1987 Beloved (Morrison)Each sorted call gets a lambda that picks one field out of a book dictionary. The last key returns a tuple, (author, year), so the two Austen books, which tie on author, are put in year order. books itself is never changed, because sorted returns a new list each time. Adding reverse=True to the year sort would list the newest book first.
Reread 10.8 Sorting with key if your result differed.
Question 13 · Short answer
apply_twice. Write apply_twice(func, value), which calls func on value and then calls func again on the result, returning the final value. Test it with three different functions, at least one of them a lambda.
Check your reasoning
One possible solution:
def apply_twice(func, value):
"""Call func on value, then call func again on that result."""
return func(func(value))
def double(number):
return number * 2
def add_exclamation(text):
return text + "!"
print(apply_twice(double, 5))
print(apply_twice(add_exclamation, "Hello"))
print(apply_twice(lambda n: n ** 2, 3))20
Hello!!
81func(func(value)) works from the inside out: the inner call’s result becomes the outer call’s argument. With double, 5 becomes 10, then 20. With add_exclamation, "Hello" becomes "Hello!", then "Hello!!". The lambda squares 3 to get 9, then 9 to get 81. apply_twice works with numbers and strings alike because it only calls func; what happens to the value is up to the function you pass. Each test passes the function’s name without parentheses.
Reread 10.6 Functions Are Values and 10.7 lambda Expressions if your result differed.
Review questions
Question 14 · Tracing
What does this program print? For each call, say what items, bag, and max_items hold.
def pack(owner, *items, bag="backpack", max_items=3):
"""Return what fits in the bag, and how many items were left out."""
kept = items[:max_items]
left_out = len(items) - len(kept)
return f"{owner}'s {bag}: {', '.join(kept)} ({left_out} left out)"
print(pack("Ana", "book", "pen"))
print(pack("Ben", "map", "torch", "rope", "tent", bag="duffel"))
print(pack("Cal", "snack", "water", "hat", "gloves", max_items=2))Check your reasoning
Ana's backpack: book, pen (0 left out)
Ben's duffel: map, torch, rope (1 left out)
Cal's backpack: snack, water (2 left out)| Call | items |
bag |
max_items |
kept |
|---|---|---|---|---|
| Ana | ("book", "pen") |
"backpack" |
3 | both |
| Ben | ("map", "torch", "rope", "tent") |
"duffel" |
3 | first 3 |
| Cal | ("snack", "water", "hat", "gloves") |
"backpack" |
2 | first 2 |
Every string after the owner goes into the items tuple. bag and max_items come after *items, so they can only be set by keyword, and otherwise keep their defaults. Slicing a tuple, items[:max_items], works like slicing a list, and ", ".join(kept) (Day 3) joins the kept items with commas.
Reread 10.4 Any Number of Arguments: *args and 10.5 Keyword Arguments by the Handful: **kwargs if your output differed.
Question 15 · Debugging
Each call to log_event should start a new log unless one is passed in, so Monday’s log should hold only login. Instead the program prints the output below. Explain the bug and fix it.
def log_event(event, log=[]):
"""Add event to a log and return the log."""
log.append(event)
return log
monday = log_event("login")
tuesday = log_event("logout")
print("Monday:", monday)
print("Tuesday:", tuesday)Monday: ['login', 'logout']
Tuesday: ['login', 'logout']Check your reasoning
The default log=[] is made once, when def runs, and both calls share that one list. The first call appends "login" to it, the second appends "logout" to the same list, and monday and tuesday are two names for it, so both show both events. Use None as the default and make the list inside the function:
def log_event(event, log=None):
"""Add event to a log and return the log. With no log, start a new one."""
if log is None:
log = []
log.append(event)
return log
monday = log_event("login")
tuesday = log_event("logout")
print("Monday:", monday)
print("Tuesday:", tuesday)Monday: ['login']
Tuesday: ['logout']if log is None: log = [] runs on every call, so each call without a log gets its own new list.
Reread 10.3 The Mutable Default Argument Trap if you missed it.
Question 16 · Multiple choice
prices = {"tea": 2.5, "coffee": 3.0, "juice": 2.25}. Which expression gives 'juice', the name of the cheapest drink?
min(prices)min(prices.values())min(prices, key=prices.get)max(prices, key=prices.get)
Check your reasoning
C.
>>> prices = {"tea": 2.5, "coffee": 3.0, "juice": 2.25}
>>> min(prices)
'coffee'
>>> min(prices.values())
2.25
>>> min(prices, key=prices.get)
'juice'
>>> max(prices, key=prices.get)
'coffee'Going through a dictionary gives its keys, the names. key=prices.get makes min compare each name by its price, and min returns the name, not the price.
- A compares the names themselves, alphabetically:
'coffee'comes first. - B finds the cheapest price, 2.25, but not which drink it belongs to.
- D uses the right key but finds the most expensive drink,
'coffee'.
Reread 10.9 min() and max() with key if you chose differently.
Question 17 · Tracing
A dictionary’s values can be functions. What does this program print?
operations = {
"double": lambda n: n * 2,
"square": lambda n: n * n,
"negate": lambda n: -n,
}
value = 3
for name, operation in operations.items():
value = operation(value)
print(name, value)Check your reasoning
double 6
square 36
negate -36name |
operation |
value before |
value after |
|---|---|---|---|
"double" |
lambda n: n * 2 |
3 | 6 |
"square" |
lambda n: n * n |
6 | 36 |
"negate" |
lambda n: -n |
36 | -36 |
items() gives each name with its function, in the order the dictionary was written. operation(value) calls the function, and the result is stored back in value, so each step starts from the last one’s result, like an accumulator. The functions are values stored in a dictionary just as numbers or strings would be.
Reread 10.6 Functions Are Values and 10.7 lambda Expressions if your output differed.
Question 18 · Short answer
Instead of a tuple key, a classmate sorts the students from 10.8 by grade, with names in order inside each grade, using two sorted calls:
students = [
{"name": "Maya", "grade": 10, "score": 88},
{"name": "Leo", "grade": 9, "score": 95},
{"name": "Ava", "grade": 10, "score": 95},
{"name": "Sam", "grade": 9, "score": 72},
]
by_name = sorted(students, key=lambda student: student["name"])
by_grade = sorted(by_name, key=lambda student: student["grade"])
for student in by_grade:
print(student["grade"], student["name"])9 Leo
9 Sam
10 Ava
10 MayaExplain why this works, and what would go wrong if the two sorted calls were done in the other order.
Check your reasoning
Model answer: the first call puts every student in name order. The second sorts by grade, and because the sort is stable, students who tie on grade keep the order they already had, which is name order. So within each grade the names stay alphabetical. In the other order, sorting by grade first and by name second, the final sort would be by name alone: Ava, Leo, Maya, Sam, with the grades mixed up. The last sort decides the main order, and earlier sorts only break its ties.
A correct answer uses stability (equal keys keep their existing order) and says that the tie-breaking field must be sorted first.
Reread 10.8 Sorting with key if your reasoning differed.
Question 19 · Short answer
The documentation for print shows print(*objects, sep=' ', end='\n', file=None, flush=False). Using the signature, explain why print("a", "b", "-") does not put a - between a and b, and how to make it do so.
Check your reasoning
>>> print("a", "b", "-")
a b -
>>> print("a", "b", sep="-")
a-b
>>> print("a", "b", sep="-", end="!\n")
a-b!Model answer: *objects collects every positional argument into the values to print, so "-" is just a third value, printed after a space. sep comes after *objects, so it is keyword-only: the only way to set it is by name, print("a", "b", sep="-"). The same goes for end, which can be set together with sep in one call.
A correct answer says that the positional "-" is collected by *objects, and that sep must be given by keyword because it comes after the starred parameter.
Reread 10.10 Reading a Function Signature and 10.2 Keyword Arguments if your reasoning differed.
Go further
The official Python Tutorial covers today’s ideas in More Control Flow Tools: More on Defining Functions, including default values, keyword arguments, special parameters (/ and *), arbitrary argument lists, and lambda expressions. The Sorting Techniques guide shows more ways to use key.
Reflection
Check each statement. If the answer is “not yet,” reread the linked subsection.
- I can give a parameter a default value and say which parameters must come first. Reread
- I can call a function with keyword arguments, including skipping a default. Reread
- I can explain why a list default is shared between calls and fix it with None. Reread
- I can write a function that takes any number of arguments with *args. Reread
- I can collect keyword arguments with **kwargs and explain keyword-only parameters. Reread
- I can pass a function to another function without calling it. Reread
- I can write a lambda and say when a def is better. Reread
- I can sort a list of dictionaries by one field, and by two. Reread
- I can find the item with the largest or smallest key with max and min. Reread
- I can read a signature with / and * and write a valid call. Reread