Functions

Functions are reusable blocks of code. In this notebook, we’ll build a library of golf utility functions.

What You’ll Learn

  • Defining functions with def
  • Parameters, return values, and type hints
  • Default arguments and keyword arguments
  • Returning multiple values
  • Lambda functions
  • The if __name__ == "__main__" pattern

Concept: Why Functions?

Without functions, you’d copy-paste the same scoring logic every time you need it. Functions solve three problems:

  1. Reusability — Write once, use everywhere
  2. Abstraction — Hide complexity behind a simple name (calculate_handicap() is easier to read than 10 lines of math)
  3. Testability — You can verify a function works correctly in isolation

Think of a function like a specific club in your bag — it has a clear purpose, you know what it does, and you reach for it when the situation calls for it.


Code: Defining Functions

Basic Structure

# Simple function — no parameters, no return value
def print_welcome():
    print("Welcome to the Golf Data Science Course!")

print_welcome()
# Parameters and return value
def calculate_relative_score(score: int, par: int) -> int:
    """Calculate score relative to par."""
    return score - par

result = calculate_relative_score(78, 72)
print(f"78 on a par 72 = {result:+d}")  # +6
# Type hints make your code self-documenting
# They don't enforce anything at runtime — they're for readability and tooling
def format_score(score: int, par: int) -> str:
    """Format a score relative to par (e.g., +3, -1, E)."""
    diff = score - par
    if diff > 0:
        return f"+{diff}"
    elif diff < 0:
        return str(diff)
    return "E"

print(format_score(68, 72))  # -4
print(format_score(72, 72))  # E
print(format_score(78, 72))  # +6

Default Arguments

Give parameters default values so callers don’t always have to specify them.

def calculate_handicap_differential(
    score: int,
    course_rating: float,
    slope_rating: int = 113,  # 113 is the standard (average) slope
) -> float:
    """Calculate a handicap differential for a single round.
    
    Formula: (Score - Course Rating) × 113 / Slope Rating
    """
    return (score - course_rating) * 113 / slope_rating


# North Park: course rating 71.1, slope 117
diff = calculate_handicap_differential(82, 71.1, 117)
print(f"Score 82 at North Park: differential = {diff:.1f}")

# Using default slope (113)
diff = calculate_handicap_differential(82, 71.1)
print(f"Score 82 at average course: differential = {diff:.1f}")

Keyword Arguments

Use parameter names to make function calls more readable and order-independent.

# These are all equivalent
diff1 = calculate_handicap_differential(82, 71.1, 117)
diff2 = calculate_handicap_differential(score=82, course_rating=71.1, slope_rating=117)
diff3 = calculate_handicap_differential(82, slope_rating=117, course_rating=71.1)

print(f"All the same: {diff1:.1f} = {diff2:.1f} = {diff3:.1f}")

Returning Multiple Values

Python functions can return multiple values as a tuple.

def analyze_round(scores: list[int], pars: list[int]) -> tuple[int, int, dict]:    """Analyze a round and return total, relative score, and scoring breakdown."""    total = sum(scores)    total_par = sum(pars)    relative = total - total_par    breakdown = {        "albatrosses": 0,        "eagles": 0,        "birdies": 0,        "pars": 0,        "bogeys": 0,        "doubles+": 0,    }    for score, par in zip(scores, pars):        diff = score - par        if diff <= -3:            breakdown["albatrosses"] += 1        elif diff == -2:            breakdown["eagles"] += 1        elif diff == -1:            breakdown["birdies"] += 1        elif diff == 0:            breakdown["pars"] += 1        elif diff == 1:            breakdown["bogeys"] += 1        else:            breakdown["doubles+"] += 1    return total, relative, breakdown# Unpack the return valuespars   = [4, 3, 5, 4, 4, 3, 5, 4, 4, 4, 3, 5, 4, 4, 3, 5, 4, 4]scores = [4, 2, 5, 5, 4, 3, 6, 4, 4, 5, 3, 4, 5, 4, 3, 5, 4, 5]total, relative, breakdown = analyze_round(scores, pars)print(f"Score: {total} ({format_score(total, sum(pars))})")print(f"Breakdown: {breakdown}")

Variable Arguments (*args)

Accept any number of arguments.

def average_score(*scores: int) -> float:
    """Calculate the average of any number of scores."""
    if not scores:
        return 0.0
    return sum(scores) / len(scores)

print(f"Average: {average_score(78, 82, 75, 80):.1f}")
print(f"Single round: {average_score(72):.1f}")

Lambda Functions

Small, anonymous functions for simple operations — often used for sorting and filtering.

# Sort players by handicapplayers = [    {"name": "Bear Woods", "handicap": 2.1},    {"name": "Brian Kolowitz", "handicap": 13.9},    {"name": "Sam Shanks", "handicap": 18.7},    {"name": "Bobby Bogey", "handicap": 25.3},]# Lambda: a one-line functionsorted_players = sorted(players, key=lambda p: p["handicap"])for p in sorted_players:    print(f"{p['name']:20s} HCP {p['handicap']:.1f}")
# Filter: find all sub-80 rounds
round_scores = [78, 82, 75, 80, 77, 83, 79, 76, 81, 68]

sub_80 = list(filter(lambda s: s < 80, round_scores))
print(f"Sub-80 rounds: {sub_80}")

# Map: convert scores to relative-to-par
par = 72
relative = list(map(lambda s: s - par, round_scores))
print(f"Relative to par: {relative}")

Building a Reusable Module

Let’s combine our functions into something reusable. In a real project, you’d save this as golf_utils.py.

# ── Golf Utilities ────────────────────────────────────────────────

def format_score(score: int, par: int) -> str:
    """Format a score relative to par (e.g., +3, -1, E)."""
    diff = score - par
    if diff > 0:
        return f"+{diff}"
    elif diff < 0:
        return str(diff)
    return "E"


def scoring_name(score: int, par: int) -> str:
    """Return the golf name for a hole score."""
    names = {-3: "Albatross", -2: "Eagle", -1: "Birdie", 0: "Par",
             1: "Bogey", 2: "Double Bogey", 3: "Triple Bogey"}
    return names.get(score - par, f"{score - par:+d}")


def handicap_differential(
    score: int, course_rating: float, slope_rating: int = 113
) -> float:
    """Calculate handicap differential: (Score - CR) × 113 / Slope."""
    return (score - course_rating) * 113 / slope_rating


def calculate_handicap(differentials: list[float]) -> float:
    """Calculate handicap index from a list of differentials.
    
    Uses the best 8 of the last 20 differentials.
    """
    if len(differentials) < 5:
        return max(differentials)  # Simplified for few rounds
    
    # Take the best (lowest) 8 of up to 20 most recent
    recent = differentials[-20:]
    best_8 = sorted(recent)[:8]
    return sum(best_8) / len(best_8)


# ── Test it ───────────────────────────────────────────────────────

# Simulate 10 rounds at North Park (CR 71.1, Slope 117)
scores = [82, 78, 85, 80, 79, 83, 77, 81, 84, 76]
diffs = [handicap_differential(s, 71.1, 117) for s in scores]

print("Round differentials:")
for score, diff in zip(scores, diffs):
    print(f"  Score {score} → diff {diff:.1f}")

hcp = calculate_handicap(diffs)
print(f"\nHandicap Index: {hcp:.1f}")

The if __name__ == "__main__" Pattern

When you save functions in a .py file, you often want to include test code that only runs when the file is executed directly — not when it’s imported.

# golf_utils.py

def format_score(score: int, par: int) -> str:
    # ... function code ...

def scoring_name(score: int, par: int) -> str:
    # ... function code ...

if __name__ == "__main__":
    # This only runs when you do: python golf_utils.py
    # It does NOT run when you do: from golf_utils import format_score
    print(format_score(68, 72))  # -4
    print(scoring_name(3, 4))    # Birdie

AI: Functions and Code Design

Exercise 1: AI-Generated Handicap Calculator

Try this prompt:

“Write a Python function that calculates a USGA handicap index. It should take a list of (score, course_rating, slope_rating) tuples, use the official formula, and handle edge cases like fewer than 5 rounds. Include type hints and a docstring.”

Evaluate: - Does it use the correct formula? (Check against the USGA rules) - Does it handle the “best N of 20” rule correctly? - How does it compare to our calculate_handicap function above?

Exercise 2: Refactoring with AI

Take this repetitive code and ask AI to refactor it into functions:

scores1 = [4, 3, 5, 5, 4, 3, 6, 4, 4]
pars1 = [4, 3, 5, 4, 4, 3, 5, 4, 4]
total1 = sum(scores1)
birdies1 = sum(1 for s, p in zip(scores1, pars1) if s < p)
print(f"Round 1: {total1}, {birdies1} birdies")

scores2 = [5, 3, 4, 4, 5, 3, 5, 5, 4]
pars2 = [4, 3, 5, 4, 4, 3, 5, 4, 4]
total2 = sum(scores2)
birdies2 = sum(1 for s, p in zip(scores2, pars2) if s < p)
print(f"Round 2: {total2}, {birdies2} birdies")

“Refactor this code to eliminate duplication. Create a function and use it for both rounds.”

This teaches you to recognize when AI is good at mechanical refactoring.

Exercise 3: Explain Lambda

If lambda functions feel confusing, ask AI:

“Explain Python lambda functions with 5 examples, starting simple and getting progressively more complex. Show the equivalent def function for each lambda.”

# Paste and test AI-generated code here

Summary

  • def function_name(param: type) -> return_type: — function definition with type hints
  • Default arguments let callers skip optional parameters
  • Keyword arguments make calls more readable: handicap_differential(score=82, course_rating=72.4)
  • Functions can return multiple values as tuples
  • *args accepts variable number of arguments
  • Lambda functions are one-line anonymous functions, useful for sorted(), filter(), map()
  • if __name__ == "__main__" separates library code from test code
  • Save reusable functions in a .py module (like golf_utils.py)

Next topic: Working with Files — reading and writing golf data in CSV and JSON.

Get the Complete Course Bundle

All notebooks, the full golf dataset, and new tutorials — straight to your inbox.