Index Ai Academy
Phase 0 · Tools · Python core

Functions as values

Store functions, pass them in with key=, and accept any number of arguments.

About 35 minutes · Free lesson

Start this lesson

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

  1. A function is a value
  2. A table of functions
  3. A calculator from a table
  4. Handing a function to another function: key=
  5. Small functions on one line: lambda
  6. map and filter, and why comprehensions usually win
  7. The highest bid
  8. Any number of arguments: args and kwargs
  9. Worked example: a shop assistant's tools
  10. 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)
It runs right here in your browser, with nothing to install.

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.

Quick check
What does this print?
measure = len
print(measure(["tent", "stove", "lamp"]))
  1. ['tent', 'stove', 'lamp']
  2. len
  3. 3
  4. An error
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.

Drill

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
In the lesson, your code is checked as you write it.

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.

Try it now

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.

Quick check
What does this print?
words = ["bivvy", "tent", "hammock"]
print(sorted(words, key=lambda w: len(w)))
  1. ['bivvy', 'hammock', 'tent']
  2. ['tent', 'bivvy', 'hammock']
  3. [4, 5, 7]
  4. ['hammock', 'bivvy', 'tent']
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.

Drill

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 = []
In the lesson, your code is checked as you write it.

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))
Quick check
What does this print?
def count(*items):
    return len(items)

print(count("a", "b", "c"), count())
  1. 3 0
  2. 1 0
  3. An error
  4. 3 None
Show the answer

3 0

*items gathers the arguments into a tuple: three items the first time, none the second.

Worked example

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))
In the lesson you can change this and run it.

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_tool above.
  • Web apps. A web framework keeps a table from addresses to functions. When a request for /products arrives, 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, min and max with a key= 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

Start this lesson

Try a quiz question

What's the difference between TOOLS = {"price": price_of} and TOOLS = {"price": price_of()}?

  1. The first stores the function; the second calls it once and stores its result
  2. They're the same
  3. The first is an error
  4. The second stores the function; the first stores its result

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson