Learnixo

Python Essentials for AI Engineers · Lesson 13 of 36

*args and **kwargs Explained

The Problem They Solve

Sometimes you don't know in advance how many arguments a function will receive:

Python
# Fixed arguments  can only add exactly 2 numbers
def add_two(a: float, b: float) -> float:
    return a + b

# *args  can add any number of values
def add_all(*values: float) -> float:
    return sum(values)

print(add_all(1.0))               # 1.0
print(add_all(1.0, 2.0, 3.0))    # 6.0
print(add_all(1.5, 2.5, 3.5, 4.5))  # 12.0

*args: Variable Positional Arguments

*args collects extra positional arguments into a tuple:

Python
def log_drugs(*drug_names: str) -> None:
    print(f"Logging {len(drug_names)} drugs:")
    for drug in drug_names:
        print(f"  - {drug}")

log_drugs("warfarin")
# Logging 1 drugs:
#   - warfarin

log_drugs("warfarin", "aspirin", "metformin")
# Logging 3 drugs:
#   - warfarin
#   - aspirin
#   - metformin

# Inside the function, args is a tuple
def show_type(*args):
    print(type(args))    # <class 'tuple'>
    print(args)          # ("warfarin", "aspirin")

**kwargs: Variable Keyword Arguments

**kwargs collects extra keyword arguments into a dict:

Python
def create_patient_record(**fields) -> dict:
    """Create a patient record from any keyword arguments provided."""
    return dict(fields)

record = create_patient_record(
    patient_id="P001",
    age=67,
    inr=2.4,
    medications=["warfarin", "aspirin"],
)
print(record)
# {"patient_id": "P001", "age": 67, "inr": 2.4, "medications": [...]}


# Inside the function, kwargs is a dict
def show_kwargs(**kwargs):
    print(type(kwargs))   # <class 'dict'>
    for key, value in kwargs.items():
        print(f"  {key} = {value}")

show_kwargs(model="gpt-4o", temperature=0, max_tokens=500)

Combining Parameters

Order must be: positional → *args → keyword-only → **kwargs

Python
def configure_agent(
    agent_name: str,       # Required positional
    *tools: str,           # Variable positional  any number of tool names
    model: str = "gpt-4o", # Keyword-only with default
    max_iterations: int = 8,
    **metadata,            # Any extra keyword arguments as metadata
) -> dict:
    return {
        "name": agent_name,
        "tools": tools,
        "model": model,
        "max_iterations": max_iterations,
        "metadata": metadata,
    }

result = configure_agent(
    "clinical_agent",
    "search_drug", "check_interaction", "format_summary",   # Goes into *tools
    model="gpt-4o",
    max_iterations=6,
    version="2.0",   # Goes into **metadata
    owner="pharmacy_team",
)
print(result["tools"])     # ("search_drug", "check_interaction", "format_summary")
print(result["metadata"])  # {"version": "2.0", "owner": "pharmacy_team"}

Unpacking: * and ** in Calls

The same * and ** syntax can unpack collections when calling functions:

Python
def calculate_dose(drug: str, weight_kg: float, dose_per_kg: float) -> float:
    return weight_kg * dose_per_kg

# Unpack a list/tuple as positional arguments
args = ("vancomycin", 70.0, 15.0)
dose = calculate_dose(*args)   # Same as calculate_dose("vancomycin", 70.0, 15.0)

# Unpack a dict as keyword arguments
kwargs = {"drug": "vancomycin", "weight_kg": 70.0, "dose_per_kg": 15.0}
dose = calculate_dose(**kwargs)   # Same as calculate_dose(drug="vancomycin", ...)

# Mix both
positional = ("vancomycin",)
keyword = {"weight_kg": 70.0, "dose_per_kg": 15.0}
dose = calculate_dose(*positional, **keyword)

Unpacking in Other Contexts

Python
# Merge lists
list_a = [1, 2, 3]
list_b = [4, 5, 6]
merged = [*list_a, *list_b]   # [1, 2, 3, 4, 5, 6]

# Merge dicts (Python 3.9+ also has | operator)
defaults = {"temperature": 0, "max_tokens": 500}
overrides = {"model": "gpt-4o", "temperature": 0.2}
config = {**defaults, **overrides}
# {"temperature": 0.2, "max_tokens": 500, "model": "gpt-4o"}  rightmost wins

# Unpack in list/tuple assignment
first, *rest = [1, 2, 3, 4, 5]
print(first)   # 1
print(rest)    # [2, 3, 4, 5]

head, *middle, tail = [1, 2, 3, 4, 5]
print(middle)  # [2, 3, 4]

Real Uses in AI Frameworks

LangChain callbacks:

Python
from langchain_core.callbacks import BaseCallbackHandler

class MyCallback(BaseCallbackHandler):
    def on_llm_start(self, serialized: dict, prompts: list, **kwargs) -> None:
        # **kwargs captures extra arguments LangChain may pass in the future
        # Without **kwargs, new LangChain versions adding extra args would break this
        run_id = kwargs.get("run_id")
        tags = kwargs.get("tags", [])

Forwarding arguments without listing them:

Python
from langchain_openai import ChatOpenAI

def create_model(model_name: str, **model_kwargs) -> ChatOpenAI:
    """Create a ChatOpenAI with any supported kwargs forwarded."""
    return ChatOpenAI(model=model_name, **model_kwargs)

# Caller can pass any ChatOpenAI argument
model = create_model(
    "gpt-4o",
    temperature=0,
    max_tokens=500,
    timeout=30,
    # Any future ChatOpenAI parameter works without changing create_model
)


# Decorator pattern  wrapping functions
import time
from functools import wraps

def timed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)   # Forward all args/kwargs to original function
        elapsed = round((time.time() - start) * 1000)
        print(f"{func.__name__} took {elapsed}ms")
        return result
    return wrapper

@timed
def embed_text(text: str, model: str = "text-embedding-3-small") -> list[float]:
    ...   # Whatever the real implementation does

Common Patterns

Python
# 1. Super() in class hierarchies  always use *args, **kwargs
class ClinicalAgent:
    def __init__(self, specialty: str, **kwargs):
        self.specialty = specialty

class PharmacistAgent(ClinicalAgent):
    def __init__(self, formulary_access: bool = True, **kwargs):
        super().__init__(**kwargs)   # Pass remaining kwargs up the chain
        self.formulary_access = formulary_access

agent = PharmacistAgent(formulary_access=True, specialty="oncology")


# 2. Config forwarding
def run_experiment(model: str, dataset: str, **hyperparams) -> dict:
    """hyperparams captured and forwarded to the training function."""
    results = train_model(model, dataset, **hyperparams)
    return results

run_experiment("gpt-4o", "clinical_qa", temperature=0, max_tokens=200, batch_size=32)


# 3. Flexible logging
def log(*messages: str, level: str = "INFO", **context) -> None:
    prefix = f"[{level}]"
    text = " ".join(str(m) for m in messages)
    meta = " ".join(f"{k}={v}" for k, v in context.items())
    print(f"{prefix} {text} | {meta}")

log("Agent invoked", "warfarin query", level="DEBUG", session_id="abc123", user="pharmacist")
# [DEBUG] Agent invoked warfarin query | session_id=abc123 user=pharmacist