Store, pass, and return functions deliberately; define callback contracts; and configure selection, transformation, and ranking behavior without duplicating traversal.
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
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?
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.
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.
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
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 selectedactive = 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):returnf'{player["name"]} ({player["score"]})'def transform_players(players, transform): transformed = []for player in players: transformed.append(transform(player))return transformedassert 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.
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:
predicate(player) was called before it was passed, so the selector receives one Boolean instead of a callable.
A ranking callback returns values that cannot be compared consistently.
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."""raiseNotImplementedErrordef solved_at_least(minimum):"""Return a predicate requiring at least minimum solved puzzles."""raiseNotImplementedErrordef leaderboard(players, eligible, rank_key):"""Return eligible player names ordered by descending rank key."""raiseNotImplementedError
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"] >= minimumreturn eligibledef 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]
when the factory runs and when its returned predicate runs; and
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.