Learnixo

Python Essentials for AI Engineers · Lesson 11 of 36

Defining and Calling Functions

Defining and Calling Functions

Python
# Basic function definition
def greet(name: str) -> str:
    return f"Hello, {name}"

# Call
result = greet("Pharmacist")
print(result)   # "Hello, Pharmacist"


# Function with no return value (implicitly returns None)
def log_event(event: str) -> None:
    print(f"[LOG] {event}")

log_event("Agent invoked")   # Prints log line, returns None

Parameters and Arguments

Python
# Positional arguments  order matters
def calculate_dose(drug: str, weight_kg: float, dose_per_kg: float) -> float:
    return weight_kg * dose_per_kg

# Positional call
result = calculate_dose("vancomycin", 70.0, 15.0)   # 1050.0

# Keyword arguments  order doesn't matter
result = calculate_dose(dose_per_kg=15.0, drug="vancomycin", weight_kg=70.0)

# Mixing: positional first, then keyword
result = calculate_dose("vancomycin", weight_kg=70.0, dose_per_kg=15.0)

Default Values

Python
def calculate_adjusted_dose(
    drug: str,
    weight_kg: float,
    dose_per_kg: float = 15.0,       # Default: 15 mg/kg
    renal_impairment: bool = False,  # Default: no impairment
) -> float:
    dose = weight_kg * dose_per_kg
    if renal_impairment:
        dose *= 0.75   # 25% reduction
    return round(dose, 1)

# Use all defaults
print(calculate_adjusted_dose("vancomycin", 70.0))          # 1050.0

# Override one default
print(calculate_adjusted_dose("vancomycin", 70.0, renal_impairment=True))  # 787.5

# Override all
print(calculate_adjusted_dose("vancomycin", 70.0, 20.0, True))  # 1050.0

Multiple Return Values

Python functions can return multiple values as a tuple:

Python
def evaluate_inr(inr: float) -> tuple[str, str]:
    """Return status and recommended action for a given INR."""
    if inr < 2.0:
        return "subtherapeutic", "Increase warfarin dose; re-check INR in 1 week"
    elif inr <= 3.0:
        return "therapeutic", "Continue current dose; routine monitoring"
    elif inr <= 4.5:
        return "supratherapeutic", "Hold 1 dose; re-check INR in 2-3 days"
    else:
        return "critically_elevated", "Hold warfarin; consider vitamin K; urgent review"

# Unpack the tuple
status, action = evaluate_inr(4.2)
print(f"Status: {status}")
print(f"Action: {action}")

# Or keep as tuple
result = evaluate_inr(2.4)
print(result)   # ("therapeutic", "Continue current dose...")

Docstrings

Docstrings document what a function does — used by IDEs, help(), and AI code assistants:

Python
def check_drug_interaction(drug_a: str, drug_b: str) -> dict:
    """
    Check for clinically significant interactions between two drugs.

    Args:
        drug_a: First drug (generic name preferred)
        drug_b: Second drug (generic name preferred)

    Returns:
        Dict with keys: severity (str), mechanism (str), recommendation (str)
        Returns empty dict if no interaction found.

    Raises:
        ValueError: If either drug name is empty
    """
    if not drug_a or not drug_b:
        raise ValueError("Drug names cannot be empty")
    ...

Access docstrings:

Python
help(check_drug_interaction)
print(check_drug_interaction.__doc__)

Functions as First-Class Objects

In Python, functions are objects — they can be assigned, passed, and returned:

Python
# Assign a function to a variable
formatter = str.upper
print(formatter("warfarin"))   # "WARFARIN"

# Pass a function as an argument
def apply_to_drugs(drugs: list[str], transform) -> list[str]:
    return [transform(drug) for drug in drugs]

drugs = ["warfarin", "aspirin", "metformin"]
print(apply_to_drugs(drugs, str.upper))   # ["WARFARIN", "ASPIRIN", "METFORMIN"]
print(apply_to_drugs(drugs, str.title))   # ["Warfarin", "Aspirin", "Metformin"]


# Return a function from a function (closure)
def make_dose_calculator(base_dose_mg_per_kg: float):
    """Return a function that calculates doses for a fixed base dose."""
    def calculate(weight_kg: float) -> float:
        return base_dose_mg_per_kg * weight_kg
    return calculate

vancomycin_dose = make_dose_calculator(15.0)
gentamicin_dose = make_dose_calculator(5.0)

print(vancomycin_dose(70.0))   # 1050.0
print(gentamicin_dose(70.0))   # 350.0

Functions in AI/ML Code

Python
# Pattern 1: Processing pipelines  each step is a function
def load_text(filepath: str) -> str:
    with open(filepath) as f:
        return f.read()

def clean_text(text: str) -> str:
    return text.lower().strip()

def chunk_text(text: str, size: int = 500) -> list[str]:
    return [text[i:i+size] for i in range(0, len(text), size)]

# Pipeline  compose functions
def process_document(filepath: str) -> list[str]:
    raw = load_text(filepath)
    cleaned = clean_text(raw)
    return chunk_text(cleaned)


# Pattern 2: Strategy pattern  pass behavior as a function
def evaluate_answers(
    questions: list[str],
    answers: list[str],
    judge_fn,   # Any callable that takes (question, answer) and returns float
) -> list[float]:
    return [judge_fn(q, a) for q, a in zip(questions, answers)]

def simple_length_judge(question: str, answer: str) -> float:
    return min(1.0, len(answer) / 200)   # Crude proxy for quality

scores = evaluate_answers(questions, answers, simple_length_judge)


# Pattern 3: Map/filter/reduce with functions
drug_names = ["warfarin", "aspirin", "metformin", "lisinopril"]
lengths = list(map(len, drug_names))          # [8, 7, 9, 10]
long_drugs = list(filter(lambda d: len(d) > 7, drug_names))  # ["warfarin", "metformin", "lisinopril"]

Variable Scope

Python
# Global scope
model_name = "gpt-4o"

def generate_answer(question: str) -> str:
    # Local scope  model_name from global is accessible (read-only)
    print(f"Using model: {model_name}")   # Works
    return "..."

def update_model(new_name: str) -> None:
    global model_name   # Required to assign to global variable
    model_name = new_name

# Avoid global state  pass values explicitly instead
def generate_answer_clean(question: str, model: str = "gpt-4o") -> str:
    print(f"Using model: {model}")
    return "..."

Pure Functions vs Side Effects

Pure functions: same input always gives same output, no external state modified.

Python
# Pure function  easy to test, cache, parallelize
def normalize_score(score: float, min_val: float, max_val: float) -> float:
    return (score - min_val) / (max_val - min_val)

# Impure function  has side effects (I/O, mutation, external state)
def log_and_normalize(score: float, min_val: float, max_val: float) -> float:
    print(f"Normalizing {score}")   # Side effect: I/O
    return (score - min_val) / (max_val - min_val)

# In AI code: prefer pure functions for transformations
# Keep side effects (DB writes, API calls, logging) at the edges of your system