TechByteByByte

Python Functions

Learn how to write reusable Python logic with functions, parameters, default and keyword arguments, *args/**kwargs, scope, lambdas, and comprehensions, with practical AI preprocessing examples.

#Python#Functions#Lambda#Comprehensions#AI#Python for AI

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

  • def starts a function definition; clean_text is its name.
  • text is a parameter — a placeholder for whatever value gets passed in when the function is called.
  • return sends a value back out to wherever the function was called.
  • Calling clean_text(raw) runs the function body with text = 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

print shows something on screen; return actually 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?

  • *args collects any number of extra positional arguments into a tuple.
  • **kwargs collects 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 **kwargs so 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 list representing a conversation history, or a dict representing 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 def function 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’s set deduplication 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 for loop (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

  1. Write a function truncate(text, max_chars) that returns text cut to max_chars characters, with a default of 200.
  2. Write a function call_model(prompt, **kwargs) that prints the prompt and all extra keyword arguments it received.
  3. Write a lambda that sorts a list of dicts by a "date" key, most recent first.
  4. Write a list comprehension that extracts the "role" from a list of message dictionaries.
  5. 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