Functions as values
Store functions, pass them in with key=, and accept any number of arguments.
Why it matters
When a language model uses a tool, your code receives the tool's name as text, like "get_weather", and must run the matching function. The standard way to do that is a dictionary from names to functions. The same idea sorts a catalog by price, registers web routes and answers button clicks. It all rests on one fact: in Python, a function is a value you can store and pass around, like a number or a list.
What you'll be able to do
- Store functions in variables and dictionaries, and call the one you need by name
- Sort and pick items with key=, using a named function or a lambda
- Write functions that accept any number of arguments with *args and **kwargs
- Pass a list or a dictionary into a call with * and **
What it covers
- A function is a value
- A table of functions
- A calculator from a table
- Handing a function to another function: key=
- Small functions on one line: lambda
- map and filter, and why comprehensions usually win
- The highest bid
- Any number of arguments: args and kwargs
- Worked example: a shop assistant's tools
- Where you'll meet this again
A function is a value
You've called functions by writing their name followed by parentheses. Leave the parentheses off, and you get the function itself, not its result. You can store it under another name and call it through that name:
def shout(text): return text.upper() + "!" speak = shout print(speak("tents on sale")) print(shout)
speak = shout doesn't run anything. It gives the same function a second name. The last line shows what a function looks like when printed: Python's description of it, with its name.
Because functions are values, they can go anywhere a value can: in a list, in a dictionary, into another function as an argument.
measure = len print(measure(["tent", "stove", "lamp"]))
Show the answer
3
measure is another name for the built-in len, so measure([...]) counts the 3 items.
A table of functions
A dictionary can map names to functions. Look up the name, then call what you find. Here's a small calculator that does that:
def add(a, b): return a + b def subtract(a, b): return a - b def multiply(a, b): return a * b OPERATIONS = {"+": add, "-": subtract, "*": multiply} operation = OPERATIONS["*"] print(operation(6, 7)) print(OPERATIONS["-"](10, 4))
The dictionary's values are the functions, written without parentheses. OPERATIONS["*"] finds multiply, and the (6, 7) after it calls it. Adding an operation means adding one line to the dictionary, not another elif to a long chain.
This pattern is called dispatch: picking the function to run from data. You'll meet it again when a model asks your code to run a tool by name.
A calculator from a table
Add a "/" entry to OPERATIONS that divides. Then write calculate(a, symbol, b), which looks up the symbol and returns the result, or returns None for a symbol it doesn't know.
def add(a, b): return a + b def subtract(a, b): return a - b def divide(a, b): return a / b OPERATIONS = {"+": add, "-": subtract} def calculate(a, symbol, b): pass
Handing a function to another function: key=
sorted, min and max take an optional key=: a function they call on each item to decide what to compare. Pass the function itself, without parentheses:
products = [
{"name": "Ridgeline tent", "price": 289},
{"name": "Trail mug", "price": 14},
{"name": "Camp stove", "price": 65},
]
def price_of(product):
return product["price"]
cheapest_first = sorted(products, key=price_of)
print([p["name"] for p in cheapest_first])
print(max(products, key=price_of)["name"])
sorted calls price_of on each product and orders them by the numbers that come back. A function you hand to another function, for it to call later, is a callback.
max with a dictionary is a neat trick. max(bids, key=bids.get) returns the key whose value is largest, because bids.get takes a key and gives back its value.
Watch rule inside apply_to_all: it holds the function half. Each time the loop calls rule(p), a frame for half appears.
Small functions on one line: lambda
Writing def price_of just to say "use the price" is a lot of ceremony. A lambda is a function with no name, written in one expression:
products = [
{"name": "Ridgeline tent", "price": 289, "rating": 4.6},
{"name": "Trail mug", "price": 14, "rating": 4.8},
{"name": "Camp stove", "price": 65, "rating": 4.2},
]
by_rating = sorted(products, key=lambda p: p["rating"], reverse=True)
print([p["name"] for p in by_rating])
lambda p: p["rating"] means "a function that takes p and returns p["rating"]". Use a lambda for short, one-off keys like this. Anything longer, or anything you'll reuse, deserves a proper def with a name.
words = ["bivvy", "tent", "hammock"] print(sorted(words, key=lambda w: len(w)))
Show the answer
['tent', 'bivvy', 'hammock']
The key is each word's length: 5, 4 and 7. sorted orders by those, shortest first, but returns the words themselves.
map and filter, and why comprehensions usually win
Python has two older helpers that take a function. map(f, items) calls f on every item, and filter(f, items) keeps the items for which f returns True. Both hand back their results lazily, so you wrap them in list() to see them:
prices = [289, 14, 65, 120] with_tax = list(map(lambda p: round(p * 1.08, 2), prices)) under_100 = list(filter(lambda p: p < 100, prices)) print(with_tax) print(under_100) print([round(p * 1.08, 2) for p in prices]) print([p for p in prices if p < 100])
The last two lines do the same jobs as comprehensions. Most Python programmers find those easier to read, because the whole recipe sits in one place, in the order you'd say it. You'll still meet map and filter in other people's code, so it's worth knowing what they do. They're most natural when the function already exists and has a good name: list(map(str.upper, names)).
The slip everyone makes once
When you hand a function to sorted, map or a dictionary, write its name with no parentheses. With parentheses, Python calls the function right then, and passes its result instead:
def price_of(product): return product["price"] products = [{"price": 289}, {"price": 14}] print(sorted(products, key=price_of()))
The error says price_of() is missing an argument, because Python tried to run it with nothing. The fix is key=price_of: hand over the function, and let sorted call it once per product. If you see "missing 1 required positional argument" on a line that passes a function, look for stray parentheses first.
The highest bid
bids maps each bidder's name to their bid. Set winner to the name with the highest bid using max with key=, and ranked to the names from highest bid to lowest.
bids = {"Ana": 120, "Ben": 175, "Cho": 140}
winner = None
ranked = []
Any number of arguments: *args and **kwargs
print takes as many values as you like. Your functions can too. A parameter written *args collects all the extra positional arguments into a tuple, a list you can't change. One written **kwargs collects all the extra keyword arguments into a dictionary:
def order_total(*prices, discount=0.0): return round(sum(prices) * (1 - discount), 2) print(order_total(289, 14)) print(order_total(289, 14, 65, discount=0.1)) def describe(name, **details): parts = [f"{key}: {value}" for key, value in details.items()] return name + " (" + ", ".join(parts) + ")" print(describe("Ridgeline tent", weight="2.1 kg", sleeps=2))
The names args and kwargs are only a habit; the stars do the work. Any parameter after *prices must be passed by keyword, which is why discount=0.1 has its name.
The stars also work the other way, in a call. * spreads a list into separate arguments, and ** spreads a dictionary into keyword arguments:
def label(name, price, currency="USD"): return f"{name}: {price} {currency}" row = ["Trail mug", 14] options = {"currency": "EUR"} print(label(*row, **options))
def count(*items): return len(items) print(count("a", "b", "c"), count())
Show the answer
3 0
*items gathers the arguments into a tuple: three items the first time, none the second.
Worked example: a shop assistant's tools
An assistant answers questions by running tools. It gets a tool's name and its inputs as data, so the code must find the right function and call it with those inputs.
STOCK = {"tent": 4, "mug": 0, "stove": 9}
PRICES = {"tent": 289, "mug": 14, "stove": 65}
def in_stock(item):
return STOCK.get(item, 0) > 0
def price_of(item):
return PRICES.get(item)
TOOLS = {"in_stock": in_stock, "price_of": price_of}
def call_tool(name, **inputs):
if name not in TOOLS:
return {"error": f"No tool called {name}"}
return TOOLS[name](**inputs)
print(call_tool("price_of", item="stove"))
print(call_tool("in_stock", item="mug"))
print(call_tool("refund", order=12))TOOLS maps each tool's name to its function. call_tool collects whatever inputs it's given into inputs with **, then spreads them back out with ** when it calls the tool, so item="stove" arrives as the item parameter. An unknown name returns an error the assistant can read, instead of crashing. Real tool use in Phase 1 works this way, with the name and inputs coming from the model's reply.
Where you'll meet this again
Passing functions around is everywhere once you look for it:
- Tool use. In Phase 1 a model replies with a tool's name and inputs, and your code dispatches to the matching function, exactly like
call_toolabove. - Web apps. A web framework keeps a table from addresses to functions. When a request for
/productsarrives, it looks up the function registered for that address and calls it. - Games and interfaces. "When the player presses the left arrow, run
turn_left" is a dictionary from keys to callbacks. - Data work. Every
sorted,minandmaxwith akey=uses the idea.
In each case the pattern is the same: store a function now, and call it later when the right moment arrives.
Practice what you just read
- 9 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
What's the difference between TOOLS = {"price": price_of} and TOOLS = {"price": price_of()}?