Functions
A function is a named, reusable piece of code. It takes inputs, does something, and hands back a result.
In agent work functions matter more than usual, because a tool is a function. Everything an agent can do in the outside world is a Python function that a model chose to call.
def add(a, b):
return a + b
print(add(2, 3))defstarts the definitionaddis the nameaandbare parameters- the colon opens the block, indentation contains it
returnsends a value back and ends the function
Defining and calling are different events
def greet(name):
print(f"Hello {name}")
greet("Priya")Line 1 does not run anything. It tells Python that the name greet refers to this block. The function body runs only when you call it on line 4.
That distinction seems pedantic now. It is the thing that makes decorators, callbacks and graph nodes make sense later, so it is worth holding on to.
Return values
def add(a, b):
return a + b
def show(a, b):
print(a + b)
x = add(2, 3)
y = show(2, 3)
print(x)
print(y)5
5
Noneshow prints but returns nothing, so y is None. A function without a return gives back None.
This causes a specific, common bug: you write a function that prints its result, then try to use the result and get None. Print is for you; return is for the program.
You can return several values at once:
def get_bounds():
return 0, 100
low, high = get_bounds()Arguments
def notify(ticket, channel="email", urgent=False):
print(f"{ticket} via {channel}, urgent={urgent}")
notify("T-1")
notify("T-1", "sms")
notify("T-1", urgent=True)
notify(ticket="T-1", channel="sms", urgent=True)- parameters with a default are optional
- you can pass by position or by name
- parameters with defaults must come after those without
Passing by name is worth the extra typing when there is more than one option. notify("T-1", "sms", True) is shorter and nobody reading it knows what True means.
The mutable default trap
This one is genuinely surprising, and it appears in real code.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("a"))
print(add_tag("b"))['a']
['a', 'b']The default list is created once, when the function is defined, and reused on every call. The fix is always the same:
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tagsNever use a list, dictionary or set as a default value.
*args and **kwargs
You will meet these constantly in framework code.
def show(*args, **kwargs):
print(args)
print(kwargs)
show(1, 2, 3, name="Priya", tier="diamond")(1, 2, 3)
{'name': 'Priya', 'tier': 'diamond'}*argscollects extra positional arguments into a tuple**kwargscollects extra named arguments into a dictionary
The same symbols work in the other direction, spreading a collection out into arguments:
args = {"city": "Hyderabad", "unit": "celsius"}
get_weather(**args)That last line is exactly how a framework turns a model's JSON arguments into a real call. Worth recognising.
Docstrings, which are not comments
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get the current weather for a city."""
return f"28 degrees {unit} in {city}"The string just under the def is a docstring. In ordinary Python it is documentation.
In agent code it is the tool description the model reads to decide whether to call this function. A tool with no docstring is a tool the model does not know when to use, and the failure is silent — the code runs, the agent simply never picks it.
So the rule for anything that will become a tool: full type hints, and a docstring that says what it does in one plain sentence.
Scope
def show():
message = "inside"
print(message)
show()
print(message)The last line raises NameError. Names created inside a function do not escape it.
A function can read names from outside, but assigning to one creates a new local name rather than changing the outer one. If you want a value back, return it. Reaching for global is almost always the wrong answer.
Small functions are the point
def triage(ticket):
if ticket["severity"] == "P1" and ticket["owner"] is None:
return "assign_and_page"
if ticket["severity"] == "P1":
return "page"
return "queue"One job, a clear name, an obvious return. This is easy to test, easy to reuse, and easy to hand to a framework as a tool or a graph node.
A useful check: if you cannot name a function without using "and", it is doing two things.
Coming from Java or C#
| Concept | Java / C# | Python |
|---|---|---|
| Define | public int add(int a, int b) | def add(a, b): |
| Return type | before the name | -> int after the parameters |
| Overloading | several methods, same name | not available — use defaults |
| Default values | not in Java; yes in C# | def f(x=1) |
| Named arguments | not in Java; yes in C# | f(x=1) |
| Varargs | T... | *args |
| Must live in a class | yes | no |
| Documentation | Javadoc, XML comments | docstring |
The two adjustments that matter. There is no overloading — you cannot define add twice with different parameters. Use default values or accept different types inside one function. And functions do not need a class. A file of plain functions is normal, idiomatic Python, not a design smell.
Common mistakes
- Printing instead of returning, then finding the result is
None. - A mutable default,
def f(x=[]). - Forgetting the colon or the indentation.
- No docstring on a tool function, so the model never chooses it.
- Trying to overload by defining the same name twice. The second silently replaces the first.
- Expecting an inner variable to be visible outside.
Practise this
- Write a function with two required parameters and one with a default. Call it three different ways.
- Write one function that prints and one that returns. Try to use the result of each.
- Reproduce the mutable default bug, then fix it.
- Write a tool-shaped function — type hints and a real docstring — and say out loud what a framework could build from it.