The problem: Real programs repeat jobs such as cleaning text, calling an API, or scoring a document. Copying the same instructions everywhere creates many places to make—and later fix—the same mistake.
What you will learn: A function gives one job a name and a boundary. Values enter as arguments, the body performs the work, and a result can return to the caller. Functions turn a long script into reusable, testable AI pipeline steps such as cleaning, chunking, scoring, and API calls.
1. What Are Functions?
Every function creates a small boundary around one job:
argument supplied by caller
↓
parameter name inside function
↓
function body performs the job
↓
return value sent back to caller
The function is defined once with def, but its body runs only when the
function is called. Calling it five times creates five separate executions,
each with its own local parameter values.
The Basic Idea
A function is a named, reusable block of code that can take input, do something with it, and hand back a result.
Why Reusable Logic Matters
Without functions, you’d copy-paste the same logic everywhere it’s needed — and if that logic ever needed to change, you’d have to find and fix every copy. Functions let you write logic once and call it wherever needed.
Picture It as a Machine
A function is a machine: you feed something in, it does its job, and something comes out:
graph LR
input[Input Arguments] --> func["[ Function Machine ]"]
func --> output[Returned Value]
A Familiar Example
Think of a coffee machine: you put in beans and water (input), press a button (call the function), and get coffee out (return value). You don’t need to know exactly how it works inside — you just use it.
Syntax
def function_name(parameter1, parameter2):
# do something
return result
💡 Function Type Hinting (PEP 484)
In modern AI development, we write type hints directly on functions to define what types of inputs they accept, and what type of output they return:
def function_name(parameter1: type, parameter2: type) -> return_type:
# do something
return result
This acts as a clear documentation contract, helping your editor highlight bugs if you try to pass numbers into a text-processing function.
Example
def clean_text(text: str) -> str:
"""Remove extra whitespace and lowercase the text."""
cleaned: str = text.strip().lower()
return cleaned
raw: str = " Hello WORLD "
result: str = clean_text(raw)
print(result)
Expected Output:
hello world
How It Works
defstarts a function definition;clean_textis its name.textis a parameter — a placeholder for whatever value gets passed in when the function is called.returnsends a value back out to wherever the function was called.- Calling
clean_text(raw)runs the function body withtext = raw.
🤖 How Is This Used in AI?
Nearly every AI pipeline is a chain of small functions:
text = load_document("notes.txt")
cleaned = clean_text(text)
chunks = split_into_chunks(cleaned)
embeddings = [embed(chunk) for chunk in chunks]
Each step — loading, cleaning, chunking, embedding — is its own function. This is exactly how real RAG pipelines are structured.
✅ Key Takeaway: A function turns “logic you’ll need again” into “logic you name once and call anywhere.”
2. Parameters and Arguments
What Is It?
Parameters are the named placeholders in a function’s definition. Arguments are the actual values you pass in when calling it.
def summarize(text: str, max_length: int) -> str: # text, max_length = parameters
return text[:max_length]
summarize("Python is great for AI", 10) # "Python is great for AI", 10 = arguments
🤖 How Is This Used in AI? Nearly every AI SDK call is a function call with named arguments:
response = client.messages.create(
model="gpt-4o-mini",
max_tokens=500,
temperature=0.7,
)
model, max_tokens, temperature are exactly the same concept you just
learned — parameters being filled with arguments.
3. Return Values
What Is It?
return is how a function hands a result back to the code that called it.
If there’s no return, the function silently gives back None.
def score_relevance(similarity: float) -> str:
if similarity >= 0.8:
return "high"
elif similarity >= 0.5:
return "medium"
return "low"
label: str = score_relevance(0.62)
print(label)
Expected Output:
medium
⚠️ Common Beginner Mistake:
def score_relevance(similarity: float) -> None: if similarity >= 0.8: print("high") # printed, not returned! result = score_relevance(0.9) print(result) # None — nothing was ever returned
returnactually hands the value back to be used by other code. They are not interchangeable.
🤖 How Is This Used in AI? A function that calls an LLM API always
needs to return the response — otherwise the caller has no way to use the
model’s answer anywhere else in the program.
4. Default Arguments
def call_model(prompt: str, temperature: float = 0.7, max_tokens: int = 500) -> None:
print(f"Calling model with temperature={temperature}, max_tokens={max_tokens}")
print(f"Prompt: {prompt}")
call_model("Explain RAG") # uses the defaults
call_model("Explain RAG", temperature=0.2) # overrides just one
Expected Output:
Calling model with temperature=0.7, max_tokens=500
Prompt: Explain RAG
Calling model with temperature=0.2, max_tokens=500
Prompt: Explain RAG
🤖 How Is This Used in AI? Nearly every AI SDK function ships with
sensible defaults (temperature=1.0, stream=False) so you only specify
what you actually want to change — this is precisely why you can call
client.messages.create(model="...", messages=[...]) without listing
every possible parameter.
5. Keyword Arguments
def build_prompt(context: str, question: str, tone: str = "neutral") -> str:
return f"[{tone}] Using context: {context}\nAnswer: {question}"
# Positional
print(build_prompt("Paris info", "What is the capital?"))
# Keyword — order doesn't matter, and intent is much clearer
print(build_prompt(question="What is the capital?", context="Paris info", tone="formal"))
🧠 Intuition: Keyword arguments are like labeling ingredients when you hand them to someone, instead of relying on the order you hand them over.
🤖 How Is This Used in AI? Real AI API calls almost always use keyword
arguments — model=, messages=, temperature= — because positional
order in a function with 8+ parameters would be unreadable and error-prone.
6. *args and **kwargs
What Is It?
*argscollects any number of extra positional arguments into a tuple.**kwargscollects any number of extra keyword arguments into a dictionary.
Example
from typing import Any
def log_event(event_name: str, *args: Any, **kwargs: Any) -> None:
print(f"Event: {event_name}")
print("Extra positional info:", args)
print("Extra details:", kwargs)
log_event("api_call", "gpt-4o-mini", status="success", latency_ms=320)
Expected Output:
Event: api_call
Extra positional info: ('gpt-4o-mini',)
Extra details: {'status': 'success', 'latency_ms': 320}
🤖 How Is This Used in AI?
- Wrapper functions around an LLM SDK often accept
**kwargsso they can pass any option straight through to the underlying API without needing to list every possible parameter by hand:
def call_llm(prompt: str, **kwargs: Any) -> Any:
# kwargs might be {"temperature": 0.5, "max_tokens": 300, ...}
return client.messages.create(messages=[{"role": "user", "content": prompt}], **kwargs)
This pattern is everywhere in AI frameworks (LangChain, OpenAI SDK, Anthropic SDK) because it lets a wrapper stay flexible without needing to know every option in advance.
⚠️ Common Beginner Mistake: Confusing
*args(tuple of values) with**kwargs(dict of named values) — remember: one star for unnamed extras, two stars for named extras.
7. Scope
What Is It?
Scope determines where a variable can be seen and used. A variable created inside a function normally only exists inside that function.
Example
def process_document():
local_var = "only visible inside this function"
print(local_var)
process_document()
print(local_var) # NameError: local_var is not defined
🧠 Intuition: Think of a function as a room with its own furniture —
what’s inside stays inside, unless you deliberately carry it out (with
return).
🤖 How Is This Used in AI? This is why functions return values
instead of relying on variables leaking out — if embed_text() computes an
embedding, the only way the rest of your pipeline can use it is if the
function returns it explicitly.
[!WARNING] Production Reality Check: Function Side Effects and Purity When you pass a mutable object (like a
listrepresenting a conversation history, or adictrepresenting configurations) into a function, any modifications you make to that object inside the function will affect the original object outside the function. This is called a Side Effect.For example:
def add_message(history: list[dict[str, str]], content: str) -> None: history.append({"role": "user", "content": content}) # Mutates history!In production AI architectures, side effects can introduce hard-to-trace bugs. It is often safer to write Pure Functions that take inputs, copy them, and return a new object:
def add_message_pure(history: list[dict[str, str]], content: str) -> list[dict[str, str]]: new_history = history.copy() # Independent copy! new_history.append({"role": "user", "content": content}) return new_history(Always be conscious of whether your function mutates its arguments!)
8. Lambda Functions
What Is It?
A lambda is a small, unnamed, one-line function.
square = lambda x: x * x
print(square(5)) # 25
🧠 Intuition: A lambda is a “throwaway” function — useful for a single, quick operation you don’t need to name and reuse elsewhere.
Example
results = [
{"text": "doc A", "score": 0.9},
{"text": "doc B", "score": 0.4},
{"text": "doc C", "score": 0.75},
]
# Sort search results by score, highest first
sorted_results = sorted(results, key=lambda r: r["score"], reverse=True)
for r in sorted_results:
print(r["score"], r["text"])
Expected Output:
0.9 doc A
0.75 doc C
0.4 doc B
🤖 How Is This Used in AI? Sorting search/retrieval results by relevance score is one of the most common one-line lambdas you’ll write in any RAG pipeline.
⚠️ When NOT to use one: If the logic needs more than one line, or a name would make the code clearer, write a proper
deffunction instead — lambdas that try to do too much quickly become unreadable.
9. List / Set / Dictionary Comprehensions
What Is It?
A compact way to build a new list, set, or dict from an existing collection, in a single line.
scores = [0.9, 0.3, 0.75, 0.5, 0.88]
# List comprehension
high_scores = [s for s in scores if s >= 0.7]
print(high_scores)
# Dict comprehension
score_labels = {s: ("high" if s >= 0.7 else "low") for s in scores}
print(score_labels)
# Set comprehension — same idea, but builds a set (unique values, no order)
documents = [
"Python is great for AI",
"python is great for ai", # same words, different casing
"RAG improves LLM answers",
]
unique_lowercase_docs = {doc.lower() for doc in documents}
print(unique_lowercase_docs)
Expected Output:
[0.9, 0.75, 0.88]
{0.9: 'high', 0.3: 'low', 0.75: 'high', 0.5: 'low', 0.88: 'high'}
{'python is great for ai', 'rag improves llm answers'}
How It Works
[... for ... in ...]builds a list — ordered, duplicates allowed.{key: value for ... in ...}builds a dict — labeled key-value pairs.{... for ... in ...}(no colon) builds a set — unique values, unordered. Notice the two differently-cased duplicate documents above collapsed into a single entry once lowercased, exactly like Module 2’ssetdeduplication behavior.
🧠 Intuition: All three are the same mental shape — “loop through a
collection, optionally filter, transform each item” — just aimed at a
different container. The brackets tell you which one you’re building:
[ ] list, {key: value} dict, { } set.
🤖 How Is This Used in AI?
# List comprehension: extract just the text from a list of search result dicts
texts = [r["text"] for r in search_results]
# Dict comprehension: build a lookup table of document ID -> embedding
embedding_lookup = {doc["id"]: embed(doc["text"]) for doc in documents}
# Set comprehension: collect unique source files referenced by retrieved chunks
sources = {chunk["source"] for chunk in retrieved_chunks}
# Embed every chunk of a document in one line
embeddings = [embed(chunk) for chunk in chunks]
This exact pattern — transforming a list of raw items into a list, dict, or set of processed items — appears constantly in preprocessing, deduplication, and embedding code.
⚠️ Common Beginner Mistake: Nesting too much logic inside a comprehension makes it unreadable. If you need more than one condition or transformation step, a regular
forloop (or a named function) is clearer.
10. Practical Functions for AI
Putting it together — a small, realistic preprocessing pipeline:
def clean_text(text):
"""Normalize whitespace and casing."""
return text.strip().lower()
def split_into_chunks(text, chunk_size=50):
"""Split text into fixed-size word chunks."""
words = text.split()
return [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
def build_metadata(chunk, source, chunk_index):
"""Attach metadata to a chunk, ready for storage in a vector DB."""
return {"text": chunk, "source": source, "chunk_index": chunk_index}
raw_text = "Python is a great language for building AI applications. " * 5
cleaned = clean_text(raw_text)
chunks = split_into_chunks(cleaned, chunk_size=10)
records = [build_metadata(c, "notes.txt", i) for i, c in enumerate(chunks)]
print(len(records))
print(records[0])
Expected Output:
5
{'text': 'python is a great language for building ai applications. python', 'source': 'notes.txt', 'chunk_index': 0}
🤖 This three-function chain — clean → chunk → attach metadata — is a simplified but genuinely accurate sketch of the document ingestion step of a real RAG pipeline.
Avoid Mutable Default Values
Python creates a default value once when it defines the function—not afresh on every call. A list as a default can therefore remember data from earlier calls:
# Surprising: the same list is reused.
def add_message(message, history=[]):
history.append(message)
return history
print(add_message("hello")) # ['hello']
print(add_message("again")) # ['hello', 'again']
Use None as a safe signal and create the list inside:
def add_message(message, history=None):
if history is None:
history = []
history.append(message)
return history
Now an omitted history produces a new list for each call.
Follow One Function Call
Suppose Python reaches score = similarity(query, document). It evaluates the
argument expressions, assigns their objects to the function’s parameter names,
runs the indented body, and sends the value after return back to the caller.
caller supplies arguments
↓
parameters receive references to those objects
↓
function creates its local variables and runs
↓
return value goes back to caller
↓
caller stores it in score
The local names disappear when the call finishes, but a returned object can remain alive because the caller now refers to it.
Module Summary
You can now define reusable logic with def, control what goes in
(parameters, defaults, keyword args, *args/**kwargs) and what comes out
(return), understand where variables are visible (scope), and write
compact transformations with lambdas and comprehensions.
AI Connection
Functions are the building blocks every AI pipeline is assembled from:
clean_text(), embed(), call_llm(), score_relevance(). Frameworks
like LangChain are, underneath the abstractions, mostly well-organized
collections of functions and classes doing exactly what you just wrote by
hand.
Mini Practice
- Write a function
truncate(text, max_chars)that returnstextcut tomax_charscharacters, with a default of200. - Write a function
call_model(prompt, **kwargs)that prints the prompt and all extra keyword arguments it received. - Write a lambda that sorts a list of dicts by a
"date"key, most recent first. - Write a list comprehension that extracts the
"role"from a list of message dictionaries. - Combine two functions you wrote above into a tiny 3-line pipeline and explain, in one sentence, why splitting the logic into functions (versus one long script) makes it easier to test and reuse.
Next: Module 5 — Object-Oriented Python — building model wrappers, tools, and agents as classes.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed