Functions Are Values
In Python a function is a value, like a number or a string. You can put it in a variable, pass it to another function, store it in a dictionary, or return it.
This sounds like trivia. It is actually the idea the entire agent stack is built on. Every graph node, every router, every callback, every tool is a function handed to a framework as data.
If you come from Java or C#, this is where the biggest habit change happens.
The one thing to get right: brackets
def greet(name):
return f"Hello {name}"
print(greet)
print(greet("Priya"))<function greet at 0x000001F3...>
Hello Priya| You write | You get |
|---|---|
greet | the function itself |
greet() | the result of running it |
That single pair of brackets is the whole distinction, and mixing them up is the most common mistake on this page.
say = greet
print(say("Priya"))Hello Priyasay = greet did not call anything. It gave the same function a second name.
Passing a function to a function
def apply_twice(func, value):
return func(func(value))
def add_ten(x):
return x + 10
print(apply_twice(add_ten, 5))25apply_twice has no idea what add_ten does. It only knows it can be called with one value.
That is why a framework can accept your code without knowing anything about it.
The pattern behind tool calling
This one is worth reading slowly, because it is how tool calling works underneath.
Start with two ordinary functions:
def get_weather(city):
return f"28 degrees in {city}"
def lookup_order(order_id):
return f"order {order_id} shipped"Put them in a dictionary, by name:
TOOLS = {
"get_weather": get_weather,
"lookup_order": lookup_order,
}Note there are no brackets. The dictionary holds the functions, not their results.
Now — a model replies with a tool name and some arguments. As data:
call = {"name": "get_weather", "args": {"city": "Hyderabad"}}And here is the whole trick:
name = call["name"]
args = call["args"]
result = TOOLS[name](**args)
print(result)28 degrees in HyderabadTake that line in two halves:
TOOLS[name]— looks the function up by its name, and gives you the function(**args)— calls it, spreading the dictionary out into named arguments
So TOOLS["get_weather"](**{"city": "Hyderabad"}) becomes get_weather(city="Hyderabad").
That is how text from a model becomes a real function call. When a framework "runs your tool", this is what it does.
It also replaces a long if/elif chain. Adding a tool becomes adding one dictionary entry.
Handling several calls, and unknown ones
calls = [
{"name": "get_weather", "args": {"city": "Hyderabad"}},
{"name": "lookup_order", "args": {"order_id": 88213}},
{"name": "no_such_tool", "args": {}},
]
for call in calls:
name = call["name"]
if name not in TOOLS:
print(f"Error: no tool called {name}")
continue
print(TOOLS[name](**call["args"]))28 degrees in Hyderabad
order 88213 shipped
Error: no tool called no_such_toolFour lines, and you have written the core of a tool dispatcher.
Returning a function
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))10 15multiply remembers the n it was created with, even though make_multiplier has long finished. A function carrying values from where it was defined is called a closure.
You will not write many by hand. You will read them constantly — it is how decorators and configured callbacks work.
Lambdas: tiny functions with no name
pairs = [("P3", 3), ("P1", 1), ("P2", 2)]
pairs.sort(key=lambda pair: pair[1])
print(pairs)[('P1', 1), ('P2', 2), ('P3', 3)]A lambda holds exactly one expression and has no name. lambda pair: pair[1] means "given a pair, give me its second item".
Use it for throwaway things — a sort key, a filter. If it needs a second line, write a proper def. Framework code uses lambdas heavily for wiring, which is another reason to read them comfortably.
Where this shows up in frameworks
Graph nodes are functions
def triage(state):
return {"severity": "P1"}
graph.add_node("triage", triage)You pass triage, not triage(). The framework calls it later, with the state, at the right point in the graph.
Routers are functions that return a name
def route(state):
if state["severity"] == "P1":
return "notify"
return "log"
graph.add_conditional_edges("triage", route)Your function decides where the graph goes next. You have handed over a decision, as code, for later execution.
Callbacks are functions handed over in advance
def on_token(token):
print(token, end="")
llm.invoke(prompt, callbacks=[on_token])Once you see it, the pattern is everywhere. A framework is mostly machinery for calling functions you supplied earlier.
Coming from Java or C#
C# has this already — delegates, Func<T>, Action<T>, lambdas. Same idea, less ceremony, nothing to declare.
Java before 8 did not, which is why older Java code wraps a single method in a class. Java 8 lambdas and method references (this::handle) are the same concept.
| Concept | Java | C# | Python |
|---|---|---|---|
| Function as a value | Function<A,B> | Func<A,B> | just the function |
| Anonymous function | x -> x + 1 | x => x + 1 | lambda x: x + 1 |
| Method reference | obj::method | obj.Method | obj.method |
| Wanting a callback | interface with one method | delegate | any callable |
The habit to unlearn: you do not need an interface, a base class, or a wrapper object. If a framework wants something to call, hand it the function.
Two mistakes worth seeing
Accidental brackets
graph.add_node("triage", triage())This calls triage immediately and passes its result. The framework later tries to call a dictionary and fails in a confusing way, far from the real mistake.
Late binding in a loop
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])[2, 2, 2]Not [0, 1, 2]. All three lambdas share the same i, and by the time you call them the loop has finished, so i is 2.
Capture the value with a default argument:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])[0, 1, 2]Common mistakes
- Adding brackets by accident when passing a function.
- Late binding in a loop, as above.
- Reaching for a class when a plain function would do.
- Long lambdas. If it does not fit comfortably on one line, use
def.
Practise this
- Print
greetandgreet("x")side by side and say aloud what each is. - Put two functions in a dictionary and call one by looking it up from a variable.
- Write the three-call dispatcher above, including the unknown-tool case.
- Write
make_multiplierfrom memory and explain whatdoubleis holding on to. - Reproduce the
[2, 2, 2]surprise, then fix it.