Session 29 of the Python Fundamentals series, covered in depth. Functions are values you can store and pass around, and lambda writes a tiny anonymous function inline. The session covers sorted(data, key=...), which sorts by anything you compute, and map and filter, which return lazy iterators that you wrap in list(). The traps are reaching for a lambda when the logic really wants a def, forgetting that map and filter are one-shot lazy iterators, so that printing one shows <map object at ...>, and the old cmp= habit that key= replaced. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 108 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 29
Functions are values - pass them, and let sorted, map, and filter do the looping
Objectives
You already write functions with def. This session treats a function as a value you can hand to other tools. By the end you can:
lambda for a tiny one-line function, and know when to use def instead.sorted(data, key=...), including reverse=True.map and filter, and remember to wrap them in list().map/filter.cmp= traps.Warm-up
Discussion prompt
Before we open Session 29 - Lambdas & Functional Tools: without looking back, what was the main idea of Session 28 - Dataclasses, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 28 of the Python Fundamentals series, in depth. The @dataclass decorator writes the boilerplate a plain class needs by hand: __init__, a readable __repr__, and value-based __eq__.
Section
Part 1
Concept
A function is a value, like 7 or "hi". You can store it in a variable, put it in a list, or pass it to another function.
first-class value — Something you can name, store, pass as an argument, and return. In Python, functions are first-class - they are ordinary values.
Counterexample
Discussion prompt
A function is a value, like 7 or "hi". You can store it in a variable, put it in a list, or pass it to another function.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Ranking
Put in order
Put the moves of Store a function in a variable into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. No parentheses, so yeller now refers to the same function object as shout.
Worked example
def shout(text):
return text.upper() + "!"
yeller = shout
print(yeller("hi"))
print(shout)Line 4 copies the function, not its result
Why: No parentheses, so yeller now refers to the same function object as shout.
yeller("hi") calls it just like shout would
Why: Verified by execution: prints HI! - the two names point at one function.
Printing the name alone shows the function object
Why: Verified by execution: line 6 prints <function shout at 0x...> - the value, not a call.
| expression | prints |
|---|---|
| yeller("hi") | HI! |
| shout | <function shout at 0x...> |
Comparison
Comparison matrix
From Store a function in a variable: refill the prints column from what you know. The rest of the table is as it appeared.
| expression | prints |
|---|---|
| yeller("hi") | HI! |
| shout | <function shout at 0x...> |
Concept
shout is the function; shout("hi") runs it. The parentheses are what trigger the call - the bare name is just the value.
That is exactly what lets you pass a function somewhere else and let that code add the parentheses later.
Analogy
Discussion prompt
Explain The name without () is the function itself by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
shout is the function; shout("hi") runs it. The parentheses are what trigger the call - the bare name is just the value.
Step zero
Discussion prompt
Pass a function as an argument — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: add_one is passed in as fn (no parentheses)
Answer:
Worked example
def apply_twice(fn, x):
return fn(fn(x))
def add_one(n):
return n + 1
print(apply_twice(add_one, 5))add_one is passed in as fn (no parentheses)
Why: apply_twice receives the function itself, then decides when to call it.
Inside, fn(fn(x)) calls it twice
Why: fn(5) is 6, then fn(6) is 7.
Read the output
Why: Verified by execution: 7.
| step | value |
|---|---|
| fn(5) | 6 |
| fn(6) | 7 |
| apply_twice(add_one, 5) | 7 |
Trade off
Comparison matrix
From Pass a function as an argument: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.
| step | value |
|---|---|
| fn(5) | 6 |
| fn(6) | 7 |
| apply_twice(add_one, 5) | 7 |
Concept
When a tool takes a function as an argument, you control what it does without rewriting the tool. sorted, map, and filter all work this way.
You supply a small function that answers one question - 'what should I sort by?', 'what should I turn each item into?' - and the tool does the looping.
Explain it
Discussion prompt
Explain Passing a function customizes behavior to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
When a tool takes a function as an argument, you control what it does without rewriting the tool. sorted, map, and filter all work this way.
Intuition
Think of a function as a labeled tool in a box. Storing it in a variable is putting a second label on the same tool. Passing it is handing the tool to a helper.
The helper does not need to know how the tool works - it just uses it when the moment comes. That is the whole idea behind key=, map, and filter.
Section
Part 2
Concept
A lambda is a one-line, throwaway function. lambda n: n * n means 'take n, give back n * n'.
lambda — An anonymous function written inline: lambda parameters: expression. The expression's value is returned automatically - no return keyword.
Definition probe
Sort into buckets
Every line below is part of the definition of first-class value or of lambda — one or the other, never both. Put each where it belongs.
Fill the middle
Fill in the blanks
From A lambda and its def twin — one line has had its right-hand side removed. Put it back.
square = **lambda n: n * n**
print(square(6))
def square_def(n):
return n * n
print(square_def(6))
Why: square is what everything below it consumes, so the wrong expression here fails later and somewhere else. lambda n: n * n and def square_def(n): return n * n compute the same thing.
Worked example
square = lambda n: n * n
print(square(6))
def square_def(n):
return n * n
print(square_def(6))Line 1 builds the same function as the def below
Why: lambda n: n * n and def square_def(n): return n * n compute the same thing.
Both are called with parentheses
Why: Verified by execution: both print 36.
| call | returns |
|---|---|
| square(6) | 36 |
| square_def(6) | 36 |
Blank canvas
Draw it
Draw what A lambda and its def twin just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
lambda then the parameters, then a colon, then one expression. That expression's value is what comes back - there is no return and no block.
Because it must be a single expression, a lambda cannot hold an if statement, a loop, or multiple lines. That limit is a feature: it keeps lambdas tiny.
Hypothesis
Predict first
Lambdas take arguments and defaults too is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.
Correct: A lambda can have several parameters
Why: add takes a and b just like a def would.
A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.
Worked example
add = lambda a, b: a + b
print(add(3, 4))
greet = lambda name="you": "Hi " + name
print(greet())
print(greet("Ana"))A lambda can have several parameters
Why: add takes a and b just like a def would.
It can even give a parameter a default
Why: greet's name defaults to "you" when omitted.
Read the output
Why: Verified by execution: 7, then Hi you, then Hi Ana.
| call | returns |
|---|---|
| add(3, 4) | 7 |
| greet() | Hi you |
| greet("Ana") | Hi Ana |
Error analysis
Annotate
Walk the callouts on Lambdas take arguments and defaults too. Each one is a place this is easy to get subtly wrong.
Intuition
Use a lambda when the function is so small and so local that naming it would be noise - a quick 'sort by this' you write right where it is used.
The moment you want to reuse it, test it, or explain it, give it a real name with def. A named function is easier to read and to debug.
Section
Part 3
Concept
sorted(data) gives back a new sorted list and leaves the original untouched. (That is the difference from data.sort(), which changes the list in place.)
Worked example
nums = [3, 1, 2]
s = sorted(nums)
print(s)
print(nums)sorted builds a fresh list in s
Why: s is the sorted copy; nums is not touched.
Read both lines
Why: Verified by execution: [1, 2, 3] then [3, 1, 2].
| name | value |
|---|---|
| s | [1, 2, 3] |
| nums | [3, 1, 2] |
Concept
Pass key=some_function. sorted calls that function on each item and sorts by the value it returns - not by the item itself.
key=len sorts by length. reverse=True flips the order from ascending to descending.
Sorting
Sort into buckets
These are the pieces of Session 29 - Lambdas & Functional Tools, out of order. Put each one back under the part of the lesson it belongs to.
Step zero
Discussion prompt
Sort words by length — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: No key sorts alphabetically
Answer:
Worked example
words = ["pear", "fig", "banana", "kiwi"]
print(sorted(words))
print(sorted(words, key=len))
print(sorted(words, key=len, reverse=True))No key sorts alphabetically
Why: Default order compares the strings themselves.
key=len sorts by number of letters
Why: sorted looks at len(word) for each word: fig(3), pear/kiwi(4), banana(6).
reverse=True sorts longest first
Why: Verified by execution - see the trace.
| call | key value used | result |
|---|---|---|
| sorted(words) | the word itself | ['banana', 'fig', 'kiwi', 'pear'] |
| sorted(words, key=len) | len(word) | ['fig', 'pear', 'kiwi', 'banana'] |
| sorted(words, key=len, reverse=True) | len(word) | ['banana', 'pear', 'kiwi', 'fig'] |
Pattern
Predict first
The table runs: 1 | Bo | 19 · 2 | Cy | 25
In Sort dictionaries by a field, given the rows so far: what is the next one — the row where order is 3?
Correct: 3 | Ana | 30
| order | name | age (key) |
|---|---|---|
| 1 | Bo | 19 |
| 2 | Cy | 25 |
| 3 | Ana | 30 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. For each dict p, key returns p["age"], so sorted orders by age.
Worked example
people = [
{"name": "Ana", "age": 30},
{"name": "Bo", "age": 19},
{"name": "Cy", "age": 25},
]
by_age = sorted(people, key=lambda p: p["age"])
for p in by_age:
print(p["name"], p["age"])The lambda pulls out the age of each person
Why: For each dict p, key returns p["age"], so sorted orders by age.
Loop over the sorted list
Why: Verified by execution - youngest to oldest.
| order | name | age (key) |
|---|---|---|
| 1 | Bo | 19 |
| 2 | Cy | 25 |
| 3 | Ana | 30 |
Pattern
Step through it
Step through Sort dictionaries by a field one row at a time. What is driving the change, and what would the row after the last one be?
Worked example
scores = [("Ana", 88), ("Bo", 72), ("Cy", 95)]
top = sorted(scores, key=lambda pair: pair[1], reverse=True)
print(top)pair[1] is the score in each tuple
Why: The lambda returns the second element, so sorted orders by score.
reverse=True puts the top score first
Why: Verified by execution: 95, 88, 72.
| order | pair | score (key) |
|---|---|---|
| 1 | ('Cy', 95) | 95 |
| 2 | ('Ana', 88) | 88 |
| 3 | ('Bo', 72) | 72 |
Concept
Anything callable works as a key: len, abs, str.lower, a def, or a lambda. Pick whichever reads clearest.
key=abs sorts by distance from zero; key=str.lower sorts case-insensitively.
Fill the middle
Fill in the blanks
From Sort by abs, and case-insensitively — one line has had its right-hand side removed. Put it back.
nums = [-3, 1, -2, 4]
print(sorted(nums, key=abs))
words = ["banana", "Apple", "cherry"]
print(sorted(words))
print(sorted(words, key=str.lower))
Why: nums is what everything below it consumes, so the wrong expression here fails later and somewhere else. It sorts by |n|: 1, 2, 3, 4, keeping each number's own sign.
Worked example
nums = [-3, 1, -2, 4]
print(sorted(nums, key=abs))
words = ["banana", "Apple", "cherry"]
print(sorted(words))
print(sorted(words, key=str.lower))key=abs ignores the sign
Why: It sorts by |n|: 1, 2, 3, 4, keeping each number's own sign.
Default sort puts capitals first
Why: "Apple" sorts before "banana" because 'A' (65) is below 'b' (98) in code point.
key=str.lower ignores case
Why: Verified by execution - see the trace.
| call | result |
|---|---|
| sorted(nums, key=abs) | [1, -2, -3, 4] |
| sorted(words) | ['Apple', 'banana', 'cherry'] |
| sorted(words, key=str.lower) | ['Apple', 'banana', 'cherry'] |
Concept
When two items have the same key, sorted keeps them in their original order. That is called a stable sort.
It lets you sort in steps: sort by one field, then by another, and ties from the second sort keep the first sort's order.
Worked example
pairs = [("a", 2), ("b", 1), ("c", 2), ("d", 1)]
print(sorted(pairs, key=lambda p: p[1]))Sort by the number p[1]
Why: The 1s come before the 2s.
Within each number, original order is kept
Why: Verified by execution: b before d (both 1), a before c (both 2).
| item | key p[1] | final position |
|---|---|---|
| ('b', 1) | 1 | 1 |
| ('d', 1) | 1 | 2 |
| ('a', 2) | 2 | 3 |
| ('c', 2) | 2 | 4 |
Discrimination
Sort into buckets
Sort these by key p[1], from memory, without looking back at Ties keep their original order. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Concept
key= is not just for sorted. max and min accept the same argument, so you can find the biggest or smallest by any measure you compute.
Worked example
people = [{"name": "Ana", "age": 30}, {"name": "Bo", "age": 19}]
oldest = max(people, key=lambda p: p["age"])
print(oldest["name"])max compares people by age
Why: The lambda returns each age; max keeps the person with the largest.
Read the output
Why: Verified by execution: Ana (30 beats 19).
| name | age (key) | picked? |
|---|---|---|
| Ana | 30 | yes |
| Bo | 19 | no |
Concept
A one-liner is a fine lambda. But when the key needs a tuple, a comment, or any real thought, write a named def and pass its name as key=.
Ranking
Put in order
Put the moves of A named key function into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Tuples compare left to right, so more wins wins; ties break on fewer losses (the minus flips it).
Worked example
def score_key(player):
return (player["wins"], -player["losses"])
players = [
{"name": "A", "wins": 3, "losses": 1},
{"name": "B", "wins": 3, "losses": 4},
{"name": "C", "wins": 5, "losses": 2},
]
ranked = sorted(players, key=score_key, reverse=True)
print([p["name"] for p in ranked])The key returns a tuple: wins, then fewer losses
Why: Tuples compare left to right, so more wins wins; ties break on fewer losses (the minus flips it).
Pass the name - no parentheses
Why: sorted calls score_key on each player itself.
Read the ranking
Why: Verified by execution: C (5 wins), then A and B tie on 3 wins - A has fewer losses, so A before B.
| player | wins | losses | key tuple |
|---|---|---|---|
| C | 5 | 2 | (5, -2) |
| A | 3 | 1 | (3, -1) |
| B | 3 | 4 | (3, -4) |
Pattern
Step through it
Step through A named key function one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 4
Concept
map(fn, items) runs fn on each item and yields the results. It does not give you a list - it gives a lazy map object you turn into a list with list().
lazy iterator — An object that produces values one at a time, only when asked. map and filter are lazy: they compute nothing until you loop over them or call list().
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
Why: These are the working definitions of first-class value, lambda, lazy iterator as Session 29 - Lambdas & Functional Tools uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Step zero
Discussion prompt
A map object is not a list — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: map returns a lazy object, not the answers
Answer:
Worked example
nums = [1, 2, 3, 4]
result = map(lambda n: n * 10, nums)
print(result)
print(list(result))map returns a lazy object, not the answers
Why: Nothing is multiplied yet - result just knows the recipe.
Printing it shows the object, not the values
Why: Verified by execution: line 3 prints <map object at 0x...>.
list() forces it to produce the values
Why: Verified by execution: line 4 prints [10, 20, 30, 40].
| expression | prints |
|---|---|
| result | <map object at 0x...> |
| list(result) | [10, 20, 30, 40] |
Intuition
map writes down the recipe ('multiply each by 10') but does not cook. It waits. Only when you loop over it or call list() does it actually run.
That laziness is efficient for huge data, but it surprises beginners: print a map and you see the recipe object, not the meal.
Worked example
nums = [1, 2, 3]
m = map(str, nums)
print("joined:", ", ".join(m))map(str, nums) will turn each number into text
Why: str is a function; map applies it to 1, 2, 3.
join consumes the map to build one string
Why: Verified by execution: join loops the map, so no explicit list() is needed here.
| item | str(item) | in joined string |
|---|---|---|
| 1 | '1' | 1 |
| 2 | '2' | 2 |
| 3 | '3' | 3 |
Section
Part 5
Concept
filter(fn, items) calls fn on each item and keeps only the ones where fn returns a truthy value. Like map, it is lazy - wrap it in list().
Explain it
Discussion prompt
Explain filter keeps items where the test is True to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
filter(fn, items) calls fn on each item and keeps only the ones where fn returns a truthy value. Like map, it is lazy - wrap it in list().
Pattern
Predict first
The table runs: 5 | False | no · 12 | True | yes · 8 | False | no · 130 | True | yes
In Keep the big numbers, given the rows so far: what is the next one — the row where n is 44?
Correct: 44 | True | yes
| n | n > 10 | kept? |
|---|---|---|
| 5 | False | no |
| 12 | True | yes |
| 8 | False | no |
| 130 | True | yes |
| 44 | True | yes |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Verified by execution: line 3 prints <filter object at 0x...>.
Worked example
nums = [5, 12, 8, 130, 44]
big = filter(lambda n: n > 10, nums)
print(big)
print(list(big))The lambda is the yes/no test
Why: n > 10 is True for the ones we keep.
The filter object prints as an object
Why: Verified by execution: line 3 prints <filter object at 0x...>.
list() gives the kept items
Why: Verified by execution: [12, 130, 44].
| n | n > 10 | kept? |
|---|---|---|
| 5 | False | no |
| 12 | True | yes |
| 8 | False | no |
| 130 | True | yes |
| 44 | True | yes |
Pattern
Step through it
Step through Keep the big numbers one row at a time. What is driving the change, and what would the row after the last one be?
Fill the middle
Fill in the blanks
From filter with a named test — one line has had its right-hand side removed. Put it back.
def is_even(n):
return n % 2 == 0
nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(is_even, nums))
print(evens)
print([n for n in nums if n % 2 == 0])
Why: evens is what everything below it consumes, so the wrong expression here fails later and somewhere else. filter calls is_even on each number and keeps the True ones.
Worked example
def is_even(n):
return n % 2 == 0
nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(is_even, nums))
print(evens)
print([n for n in nums if n % 2 == 0])Pass the function name as the test
Why: filter calls is_even on each number and keeps the True ones.
A comprehension does the same thing
Why: Verified by execution: both print [2, 4, 6].
| approach | result |
|---|---|
| list(filter(is_even, nums)) | [2, 4, 6] |
| [n for n in nums if n % 2 == 0] | [2, 4, 6] |
Comparison
Comparison matrix
From filter with a named test: refill the result column from what you know. The rest of the table is as it appeared.
| approach | result |
|---|---|
| list(filter(is_even, nums)) | [2, 4, 6] |
| [n for n in nums if n % 2 == 0] | [2, 4, 6] |
Section
Part 6
Concept
[expr for item in items if test] transforms (like map) and selects (like filter) in one readable line - and it hands back a real list, no list() needed.
Most Python programmers reach for a comprehension first. Save map/filter for when you already have a named function to pass.
Analogy
Discussion prompt
Explain A comprehension does map and filter at once by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
[expr for item in items if test] transforms (like map) and selects (like filter) in one readable line - and it hands back a real list, no list() needed.
Step zero
Discussion prompt
map vs comprehension — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: map needs a lambda and a list() wrapper
Answer:
Worked example
nums = [1, 2, 3, 4]
doubled_a = list(map(lambda n: n * 2, nums))
doubled_b = [n * 2 for n in nums]
print(doubled_a)
print(doubled_b)
print(doubled_a == doubled_b)map needs a lambda and a list() wrapper
Why: list(map(lambda n: n * 2, nums)) - three moving parts.
The comprehension reads left to right
Why: n * 2 for each n - fewer symbols, easier to scan.
Same result either way
Why: Verified by execution: both are [2, 4, 6, 8] and comparing them is True.
| name | value |
|---|---|
| doubled_a | [2, 4, 6, 8] |
| doubled_b | [2, 4, 6, 8] |
| doubled_a == doubled_b | True |
Ranking
Put in order
Put the moves of map + filter vs one comprehension into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. You read it inside-out: filter first, then map, then list.
Worked example
nums = [1, 2, 3, 4, 5, 6]
mf = list(map(lambda n: n * n, filter(lambda n: n % 2 == 0, nums)))
comp = [n * n for n in nums if n % 2 == 0]
print(mf)
print(comp)Nested map/filter is hard to read
Why: You read it inside-out: filter first, then map, then list.
The comprehension states it plainly
Why: Square each n, for the even ones - one line, top to bottom.
Identical output
Why: Verified by execution: both print [4, 16, 36].
| approach | result |
|---|---|
| list(map(..., filter(...))) | [4, 16, 36] |
| [n*n for n in nums if n%2==0] | [4, 16, 36] |
Blank canvas
Draw it
Draw what map + filter vs one comprehension just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Intuition
Code is read far more than it is written. A comprehension reads like a sentence; nested map(filter(...)) reads like a puzzle.
map/filter still shine when you are passing an existing named function (map(str.upper, words)), or streaming huge data lazily. Otherwise: comprehension.
Counterexample
Discussion prompt
Code is read far more than it is written. A comprehension reads like a sentence; nested map(filter(...)) reads like a puzzle.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
map/filter still shine when you are passing an existing named function (map(str.upper, words)), or streaming huge data lazily. Otherwise: comprehension.
Section
Part 7
Anomaly
Predict first
A student writes this, and it looks reasonable:
Reusing a map object after it is already spent.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: A map yields each value once. After first consumes them, nothing is left, so second is empty - not the crash you might expect, just silent [].
Materialize once into a list, then reuse the list.
Why: A map yields each value once. After first consumes them, nothing is left, so second is empty - not the crash you might expect, just silent [].
Trap
Reusing a map object after it is already spent.
nums = [1, 2, 3]
result = map(lambda n: n + 1, nums)
first = list(result)
second = list(result)
print(first)
print(second)The first list() drains it
Why: A map yields each value once. After first consumes them, nothing is left, so second is empty - not the crash you might expect, just silent [].
| name | value |
|---|---|
| first | [2, 3, 4] |
| second | [] |
Materialize once into a list, then reuse the list.
nums = [1, 2, 3]
result = list(map(lambda n: n + 1, nums))
first = result
second = result
print(first)
print(second)A list can be read as many times as you like
Why: Verified by execution: both print [2, 3, 4]. Wrap map/filter in list() the moment you need the values more than once.
| name | value |
|---|---|
| first | [2, 3, 4] |
| second | [2, 3, 4] |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Printing or indexing the map object directly.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: It is lazy, not a sequence, so m[0] raises TypeError - you never see the numbers.
Wrap it in list() first, then index or print.
Why: It is lazy, not a sequence, so m[0] raises TypeError - you never see the numbers.
Trap
Printing or indexing the map object directly.
nums = [1, 2, 3]
m = map(lambda n: n * 10, nums)
print(m[0])A map object has no indexing and no len
Why: It is lazy, not a sequence, so m[0] raises TypeError - you never see the numbers.
| you write | result |
|---|---|
| print(m) | <map object at 0x...> |
| m[0] | TypeError: 'map' object is not subscriptable |
Wrap it in list() first, then index or print.
nums = [1, 2, 3]
m = list(map(lambda n: n * 10, nums))
print(m[0])Now m is a real list
Why: Verified by execution: m[0] is 10. Convert with list() before you index, slice, len, or print the contents.
| you write | prints |
|---|---|
| m | [10, 20, 30] |
| m[0] | 10 |
Break the constraint
Discussion prompt
The rule this trap just fixed:
Verified by execution: m[0] is 10. Convert with list() before you index, slice, len, or print the contents.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
It is lazy, not a sequence, so m[0] raises TypeError - you never see the numbers.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A lambda so dense you cannot read it at a glance.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Verified by execution: ['A', 'B'].
Give the logic a name with def.
Why: Verified by execution: ['A', 'B']. The output is right, yet the tie-break rule is buried in a hard-to-name lambda.
Trap
A lambda so dense you cannot read it at a glance.
people = [
{"name": "A", "wins": 3, "losses": 1},
{"name": "B", "wins": 3, "losses": 4},
]
ranked = sorted(people, key=lambda p: (p["wins"], -p["losses"]), reverse=True)
print([p["name"] for p in ranked])It works, but it is a wall of punctuation
Why: Verified by execution: ['A', 'B']. The output is right, yet the tie-break rule is buried in a hard-to-name lambda.
| player | key tuple | rank |
|---|---|---|
| A | (3, -1) | 1 |
| B | (3, -4) | 2 |
Give the logic a name with def.
def rank_key(p):
return (p["wins"], -p["losses"])
people = [
{"name": "A", "wins": 3, "losses": 1},
{"name": "B", "wins": 3, "losses": 4},
]
ranked = sorted(people, key=rank_key, reverse=True)
print([p["name"] for p in ranked])Same result, but the intent has a name
Why: Verified by execution: ['A', 'B']. rank_key can be read, reused, and tested. Rule of thumb: if a lambda needs a second glance, make it a def.
| player | rank_key(p) | rank |
|---|---|---|
| A | (3, -1) | 1 |
| B | (3, -4) | 2 |
Error analysis
Annotate
Walk the callouts on Trap: cramming logic into a lambda. Each one is a place this is easy to get subtly wrong.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Passing a compare function like Python 2's cmp=.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: sorted has no cmp parameter anymore, so this raises TypeError before any sorting happens.
Use key= to say what to sort by.
Why: sorted has no cmp parameter anymore, so this raises TypeError before any sorting happens.
Trap
Passing a compare function like Python 2's cmp=.
data = [3, 1, 2]
print(sorted(data, cmp=lambda a, b: a - b))cmp was removed in Python 3
Why: sorted has no cmp parameter anymore, so this raises TypeError before any sorting happens.
| you write | result |
|---|---|
| sorted(data, cmp=...) | TypeError: 'cmp' is an invalid keyword argument for sort() |
Use key= to say what to sort by.
data = [3, 1, 2]
print(sorted(data, key=lambda n: n))key= computes one value per item
Why: Verified by execution: [1, 2, 3]. key describes each item; you never compare two items by hand. (Here key is trivial - plain sorted(data) would do.)
| you write | prints |
|---|---|
| sorted(data, key=lambda n: n) | [1, 2, 3] |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Calling the function instead of passing it.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The parentheses call len immediately, and len needs one argument, so it crashes before sorted can use it.
Pass the function name - no parentheses.
Why: The parentheses call len immediately, and len needs one argument, so it crashes before sorted can use it.
Trap
Calling the function instead of passing it.
words = ["hi", "hey"]
print(sorted(words, key=len()))len() runs len right now, with no argument
Why: The parentheses call len immediately, and len needs one argument, so it crashes before sorted can use it.
| you write | result |
|---|---|
| key=len() | TypeError: len() takes exactly one argument (0 given) |
Pass the function name - no parentheses.
words = ["hi", "hey"]
print(sorted(words, key=len))key=len hands sorted the function to call later
Why: Verified by execution: ['hi', 'hey']. sorted adds the parentheses itself, calling len(word) on each item.
| you write | prints |
|---|---|
| key=len | ['hi', 'hey'] |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
7 or "hi". You can store it in a variable, put it in a list, or pass it to another function.; shout is the function; shout("hi") runs it. The parentheses are what trigger the call - the bare name is just the value.; When a tool takes a function as an argument, you control what it does without rewriting the tool. sorted, map, and filter all work this way.Section
Part 8
Pattern
1. Decide what to sort by
Why: A length, a field, a distance - the one value that should decide order.
2. Write a key that returns that value
Why: A built-in (len, abs, str.lower), a lambda for one-liners, or a def when it grows.
3. Call sorted(data, key=that_function)
Why: Pass the name with no parentheses; add reverse=True for descending.
4. Remember sorted returns a new list
Why: The original is untouched, and equal keys keep their original order (stable).
Pattern
Turn each item into something? map or a comprehension
Why: [f(x) for x in items] is usually clearer than list(map(f, items)).
Keep only some items? filter or a comprehension
Why: [x for x in items if test] beats list(filter(...)) for readability.
Using map/filter? wrap in list() to get values
Why: They are lazy, one-shot iterators - printing one shows <map object at ...>.
Passing an existing named function? map/filter read well
Why: map(str.upper, words) is tidy; reach for the comprehension otherwise.
Real world
Discussion prompt
Outside this lesson: where does Session 29 - Lambdas & Functional Tools actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Transform or select a list is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 29 of the Python Fundamentals series, in depth. Functions are values you can store and pass around; lambda writes a tiny anonymous function inline; sorted(data, key=...) sorts by anything you compute; and map/filter return lazy iterators you wrap in list().
Check
What order comes out?
words = ["pear", "fig", "banana", "kiwi"]
print(sorted(words, key=len))| word | len |
|---|---|
| pear | 4 |
| fig | 3 |
| banana | 6 |
| kiwi | 4 |
Check your understanding
What does this print?
Answer: A
Why: key=len sorts by length ascending: fig(3), then pear and kiwi(4) keeping their original order, then banana(6). Verified by execution.
Discrimination
Sort into buckets
Sort these by len, from memory, without looking back at Check: sort by length. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Check
What appears on screen?
nums = [1, 2, 3, 4]
result = map(lambda n: n * 10, nums)
print(result)| wrapped in list()? | prints |
|---|---|
| no | ? |
Check your understanding
What does print(result) show?
Answer: A
Why: map returns a lazy iterator, and printing it shows the object like <map object at 0x...>, not the values. You need list(result) to see [10, 20, 30, 40]. Verified by execution.
Check
Now it is wrapped in list().
nums = [3, 1, 2]
print(list(map(lambda n: n * n, nums)))| n | n * n |
|---|---|
| 3 | 9 |
| 1 | 1 |
| 2 | 4 |
Check your understanding
What does this print?
Answer: A
Why: map applies n * n in the original order: 3 to 9, 1 to 1, 2 to 4, and list keeps that order. Verified by execution.
Check
Which items survive?
nums = [3, 1, 2]
print(list(filter(lambda n: n > 1, nums)))| n | n > 1 | kept? |
|---|---|---|
| 3 | True | yes |
| 1 | False | no |
| 2 | True | yes |
Check your understanding
What does this print?
Answer: A
Why: filter keeps items where n > 1 is True: 3 and 2 (in original order); 1 is dropped. Verified by execution.
Pattern
Step through it
Step through Check: filter keeps which? one row at a time. What is driving the change, and what would the row after the last one be?
Check
The map is turned into a list twice.
nums = [1, 2, 3]
result = map(lambda n: n + 1, nums)
first = list(result)
second = list(result)
print(second)| step | what is left in result |
|---|---|
| after first | nothing |
| second | ? |
Check your understanding
What does print(second) show?
Answer: A
Why: A map object yields each value once. first drained it, so second gets an empty list - no error, just []. Verified by execution.
Check
One of these is not a real argument.
data = [3, 1, 2]
print(sorted(data, cmp=lambda a, b: a - b))| argument | exists in Python 3? |
|---|---|
| key= | yes |
| cmp= | no |
Check your understanding
What happens?
Answer: A
Why: Python 3 removed cmp=; sorting is controlled by key=. Passing cmp= raises TypeError before any sorting. Verified by execution.
Trade off
Comparison matrix
From Check: key= vs cmp=: every row here is a choice with a cost. Fill the exists in Python 3? column, then say which row you would actually pick and what you give up for it.
| argument | exists in Python 3? |
|---|---|
| key= | yes |
| cmp= | no |
Check
Spot the extra parentheses.
words = ["hi", "hey"]
print(sorted(words, key=len()))| written | meaning |
|---|---|
| key=len | hand sorted the function |
| key=len() | call len now, with nothing |
Check your understanding
What happens with key=len()?
Answer: A
Why: len() calls len immediately with no argument, which is an error, so sorted never runs. Use key=len (no parentheses). Verified by execution.
Comparison
Comparison matrix
From Check: pass the function, do not call it: refill the meaning column from what you know. The rest of the table is as it appeared.
| written | meaning |
|---|---|
| key=len | hand sorted the function |
| key=len() | call len now, with nothing |
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Functions Are Values · lambda: Tiny Functions · sorted with a key · map: Transform Each Item · filter: Keep Some Items · Why a Comprehension Wins. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
A function is a value: store it, pass it, or hand it to sorted, map, and filter. A lambda writes a tiny one for small jobs; a def is better once the logic grows.
| You write | It means |
|---|---|
| f = shout | give the function a second name (no call) |
| lambda n: n * n | a one-line anonymous function |
| sorted(data, key=len) | sort by a computed value; add reverse=True |
| list(map(f, xs)) | apply f to each item, then make it a list |
| list(filter(f, xs)) | keep items where f is True, then listify |
| [f(x) for x in xs if t] | map + filter, usually the clearest option |
Remember: map/filter are lazy one-shot iterators - wrap them in list(). Use key=, never cmp=. And when a lambda needs a second glance, make it a def. Every snippet here was run on CPython 3.12.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.