Functions & Classes

Functions

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))
  • def starts the definition
  • add is the name
  • a and b are parameters
  • the colon opens the block, indentation contains it
  • return sends 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
None

show 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 tags

Never 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'}
  • *args collects extra positional arguments into a tuple
  • **kwargs collects 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#

ConceptJava / C#Python
Definepublic int add(int a, int b)def add(a, b):
Return typebefore the name-> int after the parameters
Overloadingseveral methods, same namenot available — use defaults
Default valuesnot in Java; yes in C#def f(x=1)
Named argumentsnot in Java; yes in C#f(x=1)
VarargsT...*args
Must live in a classyesno
DocumentationJavadoc, XML commentsdocstring

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.

Try It Yourself

Copy this into a new Colab notebook and run it. Nothing to install.

example.py
1# Basic function with type hints
2def create_prompt(user_input: str, context: str = "") -> str:
3 """Create a formatted prompt for the AI model."""
4 if context:
5 return f"Context: {context}\n\nUser: {user_input}"
6 return f"User: {user_input}"
7
8# Test the function
9prompt = create_prompt("What is Python?", "Programming tutorial")
10print(prompt)
11
12# Function as an AI tool
13def get_weather(city: str, unit: str = "celsius") -> dict:
14 """
15 Get weather for a city.
16 This could be called by an AI agent as a tool.
17 """
18 # Simulated response
19 return {
20 "city": city,
21 "temperature": 22,
22 "unit": unit,
23 "condition": "sunny"
24 }
25
26# Using the tool
27weather = get_weather("San Francisco")
28print(f"\nWeather in {weather['city']}: {weather['temperature']}° {weather['condition']}")
29
30# Function with *args and **kwargs
31def log_event(event_type: str, *args, **kwargs):
32 """Flexible logging function."""
33 print(f"[{event_type}]", *args)
34 for key, value in kwargs.items():
35 print(f" {key}: {value}")
36
37log_event("API_CALL", "Calling the model API", model="deepseek-chat", tokens=100)