FreeCampus Python

Passing Functions as Values

Store, pass, and return functions deliberately; define callback contracts; and configure selection, transformation, and ranking behavior without duplicating traversal.
python-foundations functions-call-behavior callbacks higher-order-functions
Open in Colab
  • Level: Python Foundations · Unit 5
  • Estimated time: 3.5–5 hours
  • You will learn: Distinguish a function object from a call result, pass behavior through callback contracts, return configured functions, and choose named functions or lambdas for readability.
  • Practice in: Google Colab, JupyterLab, or a local editor

1. Keep one traversal and swap the policy

A tournament organizer wants several leaderboards from the same records:

players = [
    {"name": "Ari", "score": 90, "solved": 5, "active": True},
    {"name": "Bo", "score": 75, "solved": 7, "active": False},
    {"name": "Cy", "score": 90, "solved": 6, "active": True},
]

Copying a loop for “active players,” “high scorers,” and “rank by solved puzzles” duplicates traversal. Instead, keep the loop stable and supply the part that varies as another function.

This requires precise answers to four questions:

  • Is an expression referring to a function or calling it now?
  • What value will the reusable code pass to a callback?
  • What must the callback return?
  • When is a named function clearer than a short lambda?

2. A function definition creates a value

def by_score(player):
    return player["score"]


ranking_rule = by_score

assert callable(by_score) is True
assert callable(ranking_rule) is True
assert ranking_rule.__name__ == "by_score"
assert ranking_rule(players[0]) == 90

by_score without parentheses refers to the function object. Assignment binds the same object to another name; it does not run the body. Parentheses in ranking_rule(players[0]) create a call and produce the integer 90.

Keep the distinction visible:

Expression Meaning Value produced immediately
by_score refer to behavior function object
by_score(players[0]) run behavior now 90
callable(by_score) ask whether object can be called True
by_score.__name__ inspect a useful metadata attribute "by_score"

3. Pass the function, not its early result

sorted accepts a key callback:

ranked = sorted(players, key=by_score, reverse=True)
assert [player["name"] for player in ranked] == ["Ari", "Cy", "Bo"]

Python does not call by_score while evaluating key=by_score. sorted later calls it once for each player and uses each returned score as the comparison key.

This is a different expression:

early_result = by_score(players[0])
assert early_result == 90

# sorted(players, key=early_result)

If the commented call runs, sorted eventually tries to call integer 90 and raises TypeError. The early call replaced behavior with one result.

The sorting mechanism owns repetition; the callback owns the policy for one record.

flowchart LR
  A["sorted receives players"] --> B["calls by_score for each player"]
  B --> C["callback returns comparison keys"]
  C --> D["sorted returns reordered records"]

4. A callback contract connects two functions

For sorted(players, key=by_score), the contract has two sides:

Participant Promise
sorted Call key with one list item; use the returned key for ordering.
by_score Accept one player record; return a comparable score.

The callback should not expect the complete list because sorted supplies one item. It should not return a dictionary if those dictionaries have no supported ordering. Read callback documentation as an interface between the mechanism and the supplied behavior.

Named key functions work with several built-ins:

best = max(players, key=by_score)
lowest = min(players, key=by_score)

assert best["name"] == "Ari"
assert lowest["name"] == "Bo"

Ties retain stable input order in sorted. max returns the first maximum it encounters. Unit 3 introduced sorting behavior; here the new idea is how a callable supplies the key.

Checkpoint: function object or call result

5. Store actions behind stable command names

Functions can be values in collections:

def cheer(name):
    return f"Go, {name}!"


def welcome(name):
    return f"Welcome, {name}."


commands = {
    "cheer": cheer,
    "welcome": welcome,
}

chosen = commands["cheer"]
assert chosen("Ari") == "Go, Ari!"

The dictionary stores a small, fixed set of known actions. The string chooses a function; a separate call supplies the function argument. Do not turn arbitrary user text into executable code. This pattern is useful because the allowed commands and their contracts remain explicit.

Every stored function here accepts one name and returns one string. A consistent contract lets the caller use the chosen function without a separate call shape for every key.

6. Write a reusable selector with a predicate callback

A predicate returns True or False for one item:

def is_active(player):
    return player["active"]


def has_high_score(player):
    return player["score"] >= 80

One explicit traversal can accept either policy:

def select_players(players, predicate):
    """Return players for which predicate(player) is true."""
    selected = []
    for player in players:
        if predicate(player):
            selected.append(player)
    return selected


active = select_players(players, is_active)
high_scores = select_players(players, has_high_score)

assert [player["name"] for player in active] == ["Ari", "Cy"]
assert [player["name"] for player in high_scores] == ["Ari", "Cy"]

select_players owns traversal. Each predicate owns one eligibility rule. The selector’s docstring states exactly how it calls the callback and interprets the result.

For a one-off simple condition, a comprehension is direct:

active_names = [player["name"] for player in players if player["active"]]
assert active_names == ["Ari", "Cy"]

Callbacks become valuable when callers genuinely need to swap, reuse, or inject behavior—not as a requirement to replace every readable loop.

7. A transform callback returns a new representation

def player_label(player):
    return f'{player["name"]} ({player["score"]})'


def transform_players(players, transform):
    transformed = []
    for player in players:
        transformed.append(transform(player))
    return transformed


assert transform_players(players, player_label) == [
    "Ari (90)",
    "Bo (75)",
    "Cy (90)",
]

The callback contract differs from the predicate contract. A predicate’s result decides keep/skip. A transform’s result becomes one output value. Generic names such as callback are not enough by themselves; document what its result means.

Checkpoint: callback contracts

8. Return a configured function from a factory

Lesson 4 introduced closures. A factory can return a ranking policy that remembers configuration:

def score_with_bonus(bonus_for_solved):
    def ranking_key(player):
        return player["score"] + player["solved"] * bonus_for_solved

    return ranking_key


plain_score = score_with_bonus(0)
puzzle_weighted = score_with_bonus(5)

assert plain_score(players[1]) == 75
assert puzzle_weighted(players[1]) == 110

The outer call chooses a policy setting once. The returned function applies that setting to many records:

ranked = sorted(players, key=puzzle_weighted, reverse=True)
assert [player["name"] for player in ranked] == ["Cy", "Bo", "Ari"]

This is useful when several functions need separate configurations. A normal parameter is simpler when only one direct call needs the setting.

9. Use lambda for one short readable expression

A lambda expression creates an anonymous function:

by_name = lambda player: player["name"]
assert by_name(players[0]) == "Ari"

alphabetical = sorted(players, key=lambda player: player["name"])
assert [player["name"] for player in alphabetical] == ["Ari", "Bo", "Cy"]

The expression after the colon is returned automatically. A lambda cannot contain statements such as an ordinary for, if statement, assignment, or return statement. Conditional expressions are possible, but a complicated lambda is harder to explain than a named function.

Prefer def when behavior:

  • needs a descriptive reusable name;
  • takes several steps;
  • benefits from a docstring or annotations;
  • needs separate assertions; or
  • is subtle enough to deserve explanation.

10. Read map and filter without treating them as mandatory

You may encounter callback-oriented built-ins:

names_from_map = list(map(lambda player: player["name"], players))
active_from_filter = list(filter(is_active, players))

assert names_from_map == ["Ari", "Bo", "Cy"]
assert [player["name"] for player in active_from_filter] == ["Ari", "Cy"]

In Python 3, map and filter return lazy iterators. Lesson 7 explains their consumption model. Comprehensions often state simple transformations and conditions more directly:

names = [player["name"] for player in players]
active = [player for player in players if is_active(player)]

Choose the form that communicates the operation to your readers. Function values are a tool for configurable behavior, not a style contest.

11. Callback failures reveal a mismatched contract

Three mistakes deserve separate diagnoses:

  1. predicate(player) was called before it was passed, so the selector receives one Boolean instead of a callable.
  2. A ranking callback returns values that cannot be compared consistently.
  3. A callback expects two parameters, but the mechanism supplies one item.

Do not hide these failures inside a broad try/except. Read which operation tried to call or compare which value. Unit 8 develops systematic error handling; here repair the mismatch between the two sides of the callback contract.

12. Lab: build a configurable leaderboard

Keep the source records unchanged while swapping eligibility and ranking:

def active_player(player):
    """Return whether player is active."""
    raise NotImplementedError


def solved_at_least(minimum):
    """Return a predicate requiring at least minimum solved puzzles."""
    raise NotImplementedError


def leaderboard(players, eligible, rank_key):
    """Return eligible player names ordered by descending rank key."""
    raise NotImplementedError

Use these checks:

players = [
    {"name": "Ari", "score": 90, "solved": 5, "active": True},
    {"name": "Bo", "score": 75, "solved": 7, "active": False},
    {"name": "Cy", "score": 90, "solved": 6, "active": True},
    {"name": "Di", "score": 80, "solved": 8, "active": True},
]
snapshot = [record.copy() for record in players]

def by_score(player):
    return player["score"]


def by_solved_then_score(player):
    return player["solved"], player["score"]


assert leaderboard(players, active_player, by_score) == ["Ari", "Cy", "Di"]
at_least_six = solved_at_least(6)
assert leaderboard(players, at_least_six, by_solved_then_score) == [
    "Di",
    "Bo",
    "Cy",
]
assert leaderboard(players, lambda player: True, by_score) == [
    "Ari",
    "Cy",
    "Di",
    "Bo",
]
assert players == snapshot
assert callable(at_least_six)

The eligible callback must accept one record and return a truth value. The rank_key callback must accept one record and return a comparable key. Stable sorting keeps Ari before Cy when their scores tie.

Hint: pass the callbacks onward without calling them too early

Create a list containing records for which eligible(player) is true. Then call sorted(..., key=rank_key, reverse=True) and extract names from that returned list. The predicate factory’s nested function compares player["solved"] with the remembered minimum.

Show one complete solution after attempting the lab
def active_player(player):
    """Return whether player is active."""
    return player["active"]


def solved_at_least(minimum):
    """Return a predicate requiring at least minimum solved puzzles."""
    def eligible(player):
        return player["solved"] >= minimum

    return eligible


def leaderboard(players, eligible, rank_key):
    """Return eligible player names ordered by descending rank key."""
    selected = []
    for player in players:
        if eligible(player):
            selected.append(player)
    ranked = sorted(selected, key=rank_key, reverse=True)
    return [player["name"] for player in ranked]

Checkpoint: factories and readable policies

13. Explain who owns each decision

Using the lab, identify:

  1. which function owns traversal;
  2. which callback owns eligibility;
  3. which callback owns ranking;
  4. when the factory runs and when its returned predicate runs; and
  5. why source order still matters for ties.

Then add by_name_length as a named key. Do not change leaderboard. If the new policy works by passing a different function, the mechanism and callback contracts are properly separated.

Key points

TipKey points
  • A function is a value; parentheses distinguish referring to it from calling it.
  • A callback contract states what the mechanism supplies and what the callback must return.
  • Functions can be assigned, stored in collections, passed to other functions, and returned from factories.
  • Predicates decide keep/skip; transforms produce output values; key functions produce comparison values.
  • Closures can remember callback configuration chosen by a factory call.
  • Use a lambda for one short, local expression and a named function when behavior needs explanation, reuse, or several steps.
  • Prefer a direct loop or comprehension when configurable behavior would only add ceremony.

References

Back to top