Topic 5.6
Functions Are Values: Passing Them Around
In one line
In Python a function is an object like any other: you can store it in a variable, put it in a list or dict, pass it to another function, and return it. That idea powers sorted(key=...), map, filter, small lambda functions, and the tool tables that let AI models call your code.
Think of it like this
A recipe card. The card is not the food; it is the instructions for making it. You can keep cards in a box (a list), file them under labels (a dictionary), hand one to a cook who decides when to follow it (passing it to another function), or let a helper pick the right card for you (returning it). A Python function is a card like that: you can hold it and hand it around without cooking, which means without running it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- First-class function
- A function that can be treated like any other value: stored, passed and returned.
- Higher-order function
- A function that takes a function as an argument, or returns one.
- Lambda
- A short unnamed function made from one expression, written
lambda x: x + 1. - Callable
- Anything that can be called with round brackets.
- map
- A built-in that applies a function to every item and gives the results lazily.
- filter
- A built-in that keeps only the items for which a function returns something true.
- Dispatch table
- A dictionary that maps names to functions so you can call the right one by name.
- key function
- A function given to
sorted,minormaxthat says what to compare.
Step by step
01A name with no brackets is the function itself
shout is the function object. shout("hi") calls it. You can copy the object to another name, print its __name__, and ask its type. Nothing runs until you add brackets.
def shout(text):
return text.upper() + "!"
loud = shout # no brackets: a second name, not a call
print(loud("hello")) # HELLO!
print(type(shout).__name__, shout.__name__) # function shout02Passing a function as an argument
apply takes any function and a value and calls the function on the value. The caller decides the behaviour, the helper does the plumbing.
def shout(text):
return text.upper() + "!"
def whisper(text):
return text.lower() + "..."
def apply(func, text):
return func(text)
print(apply(shout, "Hello")) # HELLO!
print(apply(whisper, "Hello")) # hello...03Functions in a dict, functions returned
Because functions are values, they can be dictionary values. Looking up a key then gives you a function you can call. A function can also decide which function to hand back.
styles = {"loud": shout, "quiet": whisper}
print(styles["loud"]("Good Morning")) # GOOD MORNING!
def pick(style):
return shout if style == "loud" else whisper
chosen = pick("quiet") # chosen is the function whisper
print(chosen("Wait")) # wait...04key= for sorting and searching
sorted, min and max accept key=function. Python calls the function once per item, and compares the results. The items themselves are returned unchanged. Pass len, not len().
words = ["banana", "fig", "cherry", "apple"]
print(sorted(words, key=len)) # shortest first
print(sorted(words, key=lambda w: w[-1])) # by last letter05lambda, map, filter and comprehensions
A lambda is just a function without a name. map transforms and filter selects. Comprehensions do both at once and are usually more readable. Use a def as soon as a lambda needs a second thought.
numbers = [1, 2, 3, 4, 5, 6]
print(list(map(lambda n: n * n, numbers))) # [1, 4, 9, 16, 25, 36]
print(list(filter(lambda n: n % 2 == 0, numbers))) # [2, 4, 6]
print([n * n for n in numbers if n % 2 == 0]) # [4, 16, 36]06Callables and the tool pattern
callable(x) is true for anything you can call. In AI code, tools are plain functions registered in a dictionary. The model returns text such as {"name": "add", "arguments": {...}}, your program looks the name up in the dictionary (and refuses names that aren't there), and calls func(**arguments). Never let a model name run arbitrary code: only the functions you put in the table can be called.
Try it yourself
- 1
Sort by your own rule
In the second example, sort
wordsby the last letter in reverse (reverse=True). Then sort bylambda w: w.count("a"). Predict first. - 2
Add a tool
In the third example, write a function
square(n)and add it toTOOLS. Add a request{"name": "square", "arguments": {"n": 9}}. Run it. - 3
Call it by mistake
Change
key=lentokey=len()and read the error. Callinglen()with no argument happens beforesortedeven starts.
Code & diagrams
shout and whisper are passed to apply, stored in a dict, and returned from pick.
Expected output
HELLO!
hello...
loud -> GOOD MORNING!
quiet -> good morning...
wait...
type: function
name: shout
True True FalseThe key function says what to compare. map is lazy, so the last line shows its type.
Expected output
['fig', 'apple', 'banana', 'cherry']
['banana', 'apple', 'fig', 'cherry']
['cherry', 'banana', 'apple', 'fig']
[1, 4, 9, 16, 25, 36]
[2, 4, 6]
[4, 16, 36]
('Ravi', 95)
mapThis is the shape of every tool-calling loop: look up the function by name, refuse unknown names, call it with the arguments dict.
Expected output
add -> 5
shout -> HELLO
unknown tool: delete_everythingBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call the function instead of passing it
Write len() with brackets in the key argument.
words = ["banana", "fig", "cherry", "apple"]
print(sorted(words, key=len()))Myth vs fact
Myth
lambda is faster or more advanced than def.
Fact
A lambda is a def with a tiny body and no name. It has the same speed. Its only advantage is brevity at the point of use.
Myth
map and filter are the Pythonic way to transform lists.
Fact
Comprehensions are the usual choice and read better. map is good when you pass an existing named function, such as map(int, strings).
Myth
print(map(...)) shows the results.
Fact
map is lazy: printing it shows a map object. Wrap it in list(...) to see the values.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Function objects have attributes:
__name__,__doc__,__defaults__,__code__,__closure__and more (Topic 5.7). They can even carry your own attributes. This is what decorators (Topic 9.6) build on. - ▸
functools.partial(func, *args, **kwargs)makes a new function with some arguments already filled in.operator.itemgetter(1)andoperator.attrgetter("name")are faster, clearer replacements for the lambdaslambda s: s[1]andlambda o: o.nameas sort keys. - ▸
sorted(key=...)calls the key function exactly once per item (not once per comparison), then sorts by the results (the decorate-sort-undecorate idea). The sort is stable (Topic 4.2). - ▸
mapandfilterreturn one-shot iterators: loop over one twice and the second pass is empty.map(f, a, b)takes several iterables and stops at the shortest one without warning; Python 3.14 addsstrict=True(as inzip) to raise an error instead.filter(None, items)keeps the truthy items. A lambda has no statements, no annotations and its__name__is<lambda>, which makes tracebacks and logs harder to read. - ▸
A tool table is also a security boundary. The model chooses a name; only names you registered can run. Validate the arguments (types, ranges) before calling, and never evaluate model text as code. Phase 16 returns to this.
Remember this
- 1
def greet(name): ...creates a function object and binds the namegreetto it. The namegreet(no brackets) is the object;greet("Asha")(with brackets) is a call to it. Soother = greetgives the same function a second name, and[greet, shout]is a list of two functions. Languages where this works are said to have first-class functions. - 2
A function that takes another function as an argument is called a higher-order function.
apply(func, text)callsfunc(text)and does not care which function it is. This lets you separate what to do to each item from how to go through the items, which is exactly whatsorted(words, key=len)does: sorted handles the ordering,keysays what to compare. - 3
A lambda is a tiny unnamed function written in one expression:
lambda w: w[-1]means "given w, return its last character". It can have several parameters but only one expression, with no statements and noreturnkeyword. (An expression is code that produces a value, likew[-1]. A statement is a whole instruction, likex = 5. The value of the expression is what the lambda returns.) Use a lambda for short throw-away functions passed straight into something likesorted. For anything longer, or that you reuse, write adefwith a name. - 4
map(func, items)appliesfuncto every item andfilter(func, items)keeps the items for whichfuncis true. Both return lazy iterators (Topic 9.1: objects that make each value only when asked), so wrap them inlist(...)to see the values. Python programmers usually prefer a comprehension (Topic 4.7) for the same job:[n * n for n in numbers if n % 2 == 0]is clearer thanlist(map(lambda n: n * n, filter(lambda n: n % 2 == 0, numbers))). Reach formapwhen you already have a named function to apply, such asmap(str, numbers). - 5
A function can also be returned from another function, which is how you pick behaviour at run time (
pick("loud")returnsshout). A dictionary of functions is a dispatch table:TOOLS[name](**arguments)looks up the function by its name and calls it. This is how AI agents (programs that let a model decide which steps to take) run tools: the model sends back a name and a dict of arguments, and your code looks up the matching function. - 6
Any object you can call with brackets is callable. Functions, lambdas, built-ins such as
len, and (later) classes and objects with a__call__method are all callable. The built-incallable(x)tells you if you can callx. A common slip is writingkey=len()(calling it now, with no argument) when you meantkey=len(passing the function itself).
Explain it without notes
What does it mean that functions are first-class values in Python?
What is a lambda, and when should you prefer def?
What is the difference between key=len and key=len()?
When would you choose a comprehension over map and filter?
How does a dispatch table work, and why is it used for tool calling?
Practice
Write apply_twice(func, value) that returns func(func(value)). Test it with a function that adds 3, and with str.upper.
Sort the list ["pear", "fig", "banana", "kiwi"] by length, and break ties alphabetically, using one sorted call.
Make a dispatch table OPS for "add", "sub" and "mul" (use lambdas) and a function calculate(op, a, b) that looks the operation up and calls it. Unknown operations should return None.
Trade-offs
- ↔
Lambda versus named function. A lambda keeps a tiny rule next to its use but has no name or docstring and shows as
<lambda>in tracebacks. A named function is better as soon as the rule has meaning of its own. - ↔
Dispatch table versus if/elif chain. A dictionary of functions is shorter and easy to extend, and lets you list the available actions. An
if/elifchain is clearer for two or three branches with different logic.
Done when you can
Done when you can pass, store and return a function, and explain brackets versus no brackets.
Done when you can sort with
key=usinglenand a lambda.Done when you can convert a
map/filterinto a comprehension.Done when you can write a dispatch table and call a function by name from a dict.
Done when you can explain why
key=len()fails.