Lesson 13 — Reusable Logic

Functions

Python functions are first-class objects — they can be assigned to variables, passed as arguments, and returned from other functions. Understanding *args, **kwargs, and closures unlocks powerful patterns.

PYTHON — Basic functions
def greet(name):
    """Return a personalised greeting."""
    return f"Sawubona, {name}!"

def average(marks):
    """Return the mean of a list of numeric marks."""
    if not marks:
        return 0.0
    return sum(marks) / len(marks)

print(greet("Thandi"))          # Sawubona, Thandi!
print(average([65, 72, 80]))     # 72.33...

Default Parameters and Keyword Arguments

PYTHON — Default parameters
def register_learner(
    name,
    course     = "Undecided",
    year       = 1,
    has_laptop = False
):
    print(f"{name} | {course} | Year {year} | Laptop: {has_laptop}")

register_learner("Thandi")                          # all defaults
register_learner("Sipho", "Java")                   # course only
register_learner("Lerato", year=2, course="SQL")    # keyword order any
register_learner(has_laptop=True, name="Bongani")   # all keyword

# MUTABLE DEFAULT ARGUMENT BUG — common Python trap:
def bad(items=[]):
    items.append(1)
    return items

print(bad())  # [1]
print(bad())  # [1, 1] !!!  — same list object reused across calls

# Correct pattern — use None as default:
def good(items=None):
    if items is None:
        items = []
    items.append(1)
    return items

*args and **kwargs

PYTHON — *args **kwargs
# *args — collect any number of positional arguments as a tuple:
def total(*marks):
    return sum(marks)

print(total(65, 72, 80, 91))  # 308 — any number of args

# **kwargs — collect keyword arguments as a dict:
def print_profile(**info):
    for key, value in info.items():
        print(f"  {key}: {value}")

print_profile(name="Thandi", city="Johannesburg", course="Python")

# Combining:
def log(message, *args, **kwargs):
    print(f"[LOG] {message}", *args, **kwargs)

log("Started", end="\n")  # forwards **kwargs to print

Functions as First-Class Objects

PYTHON — First-class functions
# Assign a function to a variable:
greet_fn = greet
print(greet_fn("Sipho"))  # Sawubona, Sipho!

# Pass a function as an argument:
marks = [65, 72, 80, 58, 91]

# sorted() accepts a key function:
sorted_marks = sorted(marks, key=lambda m: -m)  # descending
print(sorted_marks)

# Lambda — anonymous one-expression function:
to_grade = lambda m: "Pass" if m >= 50 else "Fail"
print(to_grade(74))   # Pass
print(to_grade(35))   # Fail

# Filter with lambda:
passing = list(filter(lambda m: m >= 50, marks))
print(passing)

Practice Task

Your Turn

Write a function grade_report(learners) that accepts a list of (name, mark) tuples and: returns a sorted list of (name, mark, label) tuples (sorted by mark descending), accepts an optional custom label function (defaulting to your own grade label function), accepts **kwargs that are forwarded to a print function. Test it with both default and custom label functions.

Common Mistakes

  • Default mutable arguments (list, dict, set) — always use None as default.
  • Forgetting return — the function silently returns None.
  • Confusing *args (tuple) with **kwargs (dict).
  • Lambda for multi-line logic — use a real def instead.

Professional Tip

Type-hint your function signatures for cleaner code and IDE support: def average(marks: list[float]) -> float:. Python doesn't enforce the hints but IDEs use them for autocomplete and error detection.

Mini Quiz

What does *args collect in a Python function?