Functions & Modules for AI Applications

Tech Buddy June 12, 2026 3 min read
Functions & Modules for AI Applications

In C#, code is always inside a class. A standalone function doesn't exist — you'd write a static method on a utility class, or a record for data. Python takes the opposite approach: functions are first-class citizens that live freely in modules. This difference has a significant impact on how you structure AI applications, where a lot of your code is transformation logic that doesn't naturally belong to an object.

This lesson covers how to define functions, organize them into modules, and structure a Python AI project in a way that's readable, testable, and easy to extend.

Defining Functions in Python

Python functions use the def keyword. Unlike C# methods, they don't require a class wrapper, an access modifier, or a return type declaration (though you should add type hints — covered in Lesson 1.1).

C# Example
public static string BuildPrompt(
                              string userInput,
                              string systemContext)
                          {
                              return $"Context: {systemContext}\n" +
                                     $"User: {userInput}";
                          }
                          
                          // Must live inside a class:
                          // public static class PromptUtils { ... }
Python Example
def build_prompt(
                              user_input: str,
                              system_context: str
                          ) -> str:
                              return (
                                  f"Context: {system_context}\n"
                                  f"User: {user_input}"
                              )
                          
                          # No class required — just call build_prompt()

Default Arguments and Keyword Arguments

Python functions can have default parameter values, and callers can pass arguments by name (keyword arguments). This is especially useful for AI functions that have many optional configuration parameters.

def call_llm(
                              prompt: str,
                              model: str = "claude-3-5-sonnet-20241022",
                              max_tokens: int = 1024,
                              temperature: float = 0.7,
                              system: str = "You are a helpful assistant.",
                          ) -> str:
                              """
                              Call an LLM with configurable parameters.
                              Docstrings are the Python equivalent of XML doc comments.
                              """
                              # implementation here...
                              pass
                          
                          
                          # Calling with keyword args — self-documenting and order-independent
                          response = call_llm(
                              prompt="Explain embeddings in one paragraph",
                              max_tokens=256,
                              temperature=0.2,
                          )
                          
                          # Only override what you need
                          quick_response = call_llm("What is RAG?")  # uses all defaults

Return Multiple Values

Python functions can return multiple values as a tuple — no wrapper class needed:

def analyze_response(text: str) -> tuple[str, float, int]:
                              """Returns (cleaned_text, confidence_score, token_count)."""
                              cleaned = text.strip()
                              confidence = 0.95  # hypothetical scoring
                              tokens = len(cleaned.split())
                              return cleaned, confidence, tokens  # Python auto-wraps in tuple
                          
                          # Unpack at the call site
                          text, score, count = analyze_response(raw_llm_output)
                          print(f"Got {count} tokens with {score:.0%} confidence")

Modules: Python's Namespace System

A Python module is simply a .py file. Every file you create is automatically a module. A package is a directory containing an __init__.py file, which makes it importable as a unit.

This maps roughly to C# namespaces, except the file structure IS the namespace — no separate namespace declarations needed.

C# Namespaces
// File: src/Prompts/PromptBuilder.cs
                          namespace MyAiApp.Prompts
                          {
                              public static class PromptBuilder
                              {
                                  public static string Build(string input)
                                      => $"User: {input}";
                              }
                          }
                          
                          // Usage:
                          using MyAiApp.Prompts;
                          var p = PromptBuilder.Build("hi");
Python Modules
# File: src/prompts/builder.py
                          # (directory must have __init__.py)
                          
                          def build(input: str) -> str:
                              return f"User: {input}"
                          
                          
                          # Usage in another file:
                          from src.prompts.builder import build
                          # or
                          from src.prompts import builder
                          result = builder.build("hi")

The __init__.py File

An __init__.py file marks a directory as a Python package and can control what's exported when someone imports the package:

# src/prompts/__init__.py
                          
                          # Re-export commonly used functions so callers can do:
                          # from src.prompts import build_chat_prompt
                          from .builder import build_chat_prompt
                          from .templates import SYSTEM_PROMPTS
                          
                          __all__ = ["build_chat_prompt", "SYSTEM_PROMPTS"]  # explicit public API

Practical AI Module Structure

Here's how you'd organize a real prompt utility module for an AI application. This is the kind of code you'd write in your first week of an AI engineering project:

# src/prompts/builder.py
                          
                          from __future__ import annotations
                          from typing import Optional
                          
                          
                          # Module-level constant (like a C# static readonly field)
                          DEFAULT_SYSTEM_PROMPT = """You are an expert AI assistant specializing in
                          code analysis. Be precise, concise, and cite specific line numbers
                          when referencing code."""
                          
                          
                          def build_chat_prompt(
                              user_message: str,
                              context: Optional[str] = None,
                              system_override: Optional[str] = None,
                          ) -> dict[str, str | list]:
                              """
                              Build a structured prompt dict for the Anthropic Messages API.
                          
                              Args:
                                  user_message: The user's request.
                                  context: Optional background context to prepend.
                                  system_override: Replace the default system prompt.
                          
                              Returns:
                                  A dict with 'system' and 'messages' keys ready for the API.
                              """
                              system = system_override or DEFAULT_SYSTEM_PROMPT
                          
                              # Build the user content
                              if context:
                                  user_content = f"\n{context}\n\n\n{user_message}"
                              else:
                                  user_content = user_message
                          
                              return {
                                  "system": system,
                                  "messages": [
                                      {"role": "user", "content": user_content}
                                  ],
                              }
                          
                          
                          def build_few_shot_prompt(
                              user_message: str,
                              examples: list[tuple[str, str]],
                          ) -> list[dict]:
                              """
                              Build a messages list with few-shot examples.
                          
                              Args:
                                  user_message: The actual request.
                                  examples: List of (user_input, assistant_output) tuples.
                              """
                              messages = []
                          
                              for user_ex, assistant_ex in examples:
                                  messages.append({"role": "user", "content": user_ex})
                                  messages.append({"role": "assistant", "content": assistant_ex})
                          
                              messages.append({"role": "user", "content": user_message})
                              return messages

Using the Module

# main.py (or any other file)
                          import anthropic
                          from src.prompts.builder import build_chat_prompt, build_few_shot_prompt
                          
                          client = anthropic.Anthropic()
                          
                          # Build a structured prompt
                          prompt_data = build_chat_prompt(
                              user_message="Review this Python function for potential bugs.",
                              context="This code runs in a production AI pipeline with 10k req/min.",
                          )
                          
                          response = client.messages.create(
                              model="claude-3-5-sonnet-20241022",
                              max_tokens=1024,
                              system=prompt_data["system"],
                              messages=prompt_data["messages"],
                          )
                          
                          print(response.content[0].text)

Import Patterns and Best Practices

Python has several import styles. Knowing which to use keeps your code readable and avoids namespace pollution — a common source of subtle bugs in AI codebases with large dependency trees.

# 1. Absolute import (preferred — explicit and unambiguous)
                          from src.prompts.builder import build_chat_prompt
                          
                          # 2. Relative import (use inside a package)
                          from .builder import build_chat_prompt     # same directory
                          from ..models import ChatRequest           # parent directory
                          
                          # 3. Import module, not function (good when using multiple items)
                          from src import prompts
                          prompts.build_chat_prompt(...)
                          
                          # 4. Wildcard import — AVOID in production AI code
                          from src.prompts.builder import *  # opaque, pollutes namespace
                          
                          # 5. Aliased import — good for long names or conventions
                          import anthropic as ant
                          import numpy as np  # community convention

*args and **kwargs: Flexible Function Signatures

AI wrapper functions often need to pass arbitrary arguments through to underlying SDKs. Python's *args and **kwargs handle this elegantly — similar to C#'s params keyword, but more powerful.

def call_with_retry(
                              func,          # callable to wrap
                              *args,         # positional args forwarded to func
                              retries: int = 3,
                              **kwargs,      # keyword args forwarded to func
                          ):
                              """Generic retry wrapper for any callable."""
                              for attempt in range(retries):
                                  try:
                                      return func(*args, **kwargs)
                                  except Exception as e:
                                      if attempt == retries - 1:
                                          raise
                                      print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
                          
                          
                          # The caller doesn't need to know about retry logic:
                          result = call_with_retry(
                              client.messages.create,    # the actual SDK function
                              model="claude-3-5-sonnet-20241022",
                              max_tokens=512,
                              messages=[{"role": "user", "content": "Hello"}],
                              retries=5,                 # our custom arg
                          )

Module-Level Code and the if __name__ == "__main__" Guard

Python executes module-level code when the file is imported. This is different from C#, where only explicitly called code runs. The if __name__ == "__main__" guard ensures code only runs when the file is executed directly — not when it's imported as a module.

# src/prompts/builder.py
                          
                          def build_chat_prompt(user_message: str) -> dict:
                              # ... implementation ...
                              pass
                          
                          
                          # This block runs ONLY when you do: python builder.py
                          # It does NOT run when another file does: import builder
                          if __name__ == "__main__":
                              # Quick smoke test / development runner
                              test_prompt = build_chat_prompt("Test message")
                              print(test_prompt)
                              print("Module works correctly!")

Key Takeaways

  • Python functions are first-class and don't need a class — use standalone functions for pure transformation logic
  • A Python file is a module; a directory with __init__.py is a package — your folder structure IS your namespace
  • Use keyword arguments and default values to build self-documenting AI function signatures
  • *args / **kwargs let you write flexible wrapper functions that pass through SDK parameters without knowing them upfront
  • Use __all__ in __init__.py to define a clean public API for each package
  • The if __name__ == "__main__" guard is essential for modules that double as standalone scripts