Session 29 - Lambdas & Functional Tools

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

What this lesson covers

The lesson, slide by slide

1. Lambdas & Functional Tools

Title

Python Fundamentals - Session 29

Functions are values - pass them, and let sorted, map, and filter do the looping

2. What you will be able to do

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:

  1. Store a function in a variable and pass one function into another.
  2. Write a lambda for a tiny one-line function, and know when to use def instead.
  3. Sort any data with sorted(data, key=...), including reverse=True.
  1. Use map and filter, and remember to wrap them in list().
  2. Explain why a comprehension is usually clearer than map/filter.
  3. Avoid the lazy-iterator, over-complex-lambda, and cmp= traps.

3. What survived from Session 28 - Dataclasses?

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__.

4. Functions Are Values

Section

Part 1

5. A function is a first-class value

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.

6. Break it if you can: A function is a first-class value

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.

7. What has to happen first: Store a function in a variable

Ranking

Put in order

Put the moves of Store a function in a variable into the order they have to happen.

  1. Line 4 copies the function, not its result
  2. yeller("hi") calls it just like shout would
  3. Printing the name alone shows the function object

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.

8. Store a function in a variable

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.

expressionprints
yeller("hi")HI!
shout<function shout at 0x...>

9. Fill in: prints for Store a function in a variable

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.

expressionprints
yeller("hi")HI!
shout<function shout at 0x...>

10. The name without () is the function itself

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.

11. By analogy: The name without () is the function itself

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.

12. Plan first: Pass a function as an argument

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:

  1. add_one is passed in as fn (no parentheses)
  2. Inside, fn(fn(x)) calls it twice
  3. Read the output

13. Pass a function as an argument

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.

stepvalue
fn(5)6
fn(6)7
apply_twice(add_one, 5)7

14. What each one costs: Pass a function as an argument

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.

stepvalue
fn(5)6
fn(6)7
apply_twice(add_one, 5)7

15. Passing a function customizes behavior

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.

16. Teach it back: Passing a function customizes behavior

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.

17. A function is a tool you can hand over

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.

18. lambda: Tiny Functions

Section

Part 2

19. lambda makes a function with no name

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.

20. Take the definitions apart: first-class value vs lambda

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.

first-class value
Something you can name, store, pass as an argument, and return.; In Python, functions are first-class - they are ordinary values.
lambda
An anonymous function written inline; The expression's value is returned automatically - no return keyword.
b1
Something you can name, store, pass as an argument, and return. In Python, functions are first-class - they are ordinary values.
b2
An anonymous function written inline: lambda parameters: expression. The expression's value is returned automatically - no return keyword.

21. Restore the missing line: A lambda and its def twin

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.

22. A lambda and its def twin

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.

callreturns
square(6)36
square_def(6)36

23. Draw the shape of it: A lambda and its def twin

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.

24. The shape of a lambda

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.

25. State the rule before it runs: Lambdas take arguments and defaults too

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.

26. Lambdas take arguments and defaults too

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.

callreturns
add(3, 4)7
greet()Hi you
greet("Ana")Hi Ana

27. Inspect it line by line: Lambdas take arguments and defaults too

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.

  • add takes a and b just like a def would.
  • greet's name defaults to "you" when omitted.
  • Verified by execution: 7, then Hi you, then Hi Ana.

28. lambda is a sticky note, def is a signed page

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.

29. sorted with a key

Section

Part 3

30. sorted returns a new list

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.)

31. The original list is unchanged

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].

namevalue
s[1, 2, 3]
nums[3, 1, 2]

32. key= chooses what to sort by

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.

33. Where does each piece belong: Session 29 - Lambdas & Functional Tools

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.

Functions Are Values
A function is a first-class value; Store a function in a variable; The name without () is the function itself
lambda: Tiny Functions
lambda makes a function with no name; A lambda and its def twin; The shape of a lambda
sorted with a key
sorted returns a new list; The original list is unchanged; key= chooses what to sort by
s1
Functions Are Values is where Session 29 - Lambdas & Functional Tools puts A function is a first-class value, Store a function in a variable, The name without () is the function itself. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
lambda: Tiny Functions is where Session 29 - Lambdas & Functional Tools puts lambda makes a function with no name, A lambda and its def twin, The shape of a lambda. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
sorted with a key is where Session 29 - Lambdas & Functional Tools puts sorted returns a new list, The original list is unchanged, key= chooses what to sort by. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

34. Plan first: Sort words by length

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:

  1. No key sorts alphabetically
  2. key=len sorts by number of letters
  3. reverse=True sorts longest first

35. Sort words by length

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.

callkey value usedresult
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']

36. Predict the next row: Sort dictionaries by a field

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

ordernameage (key)
1Bo19
2Cy25
3Ana30

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.

37. Sort dictionaries by a field

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.

ordernameage (key)
1Bo19
2Cy25
3Ana30

38. Watch it run: Sort dictionaries by a field

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?

  1. Step 1: order is 1
  2. Step 2: order is 2
  3. Step 3: order is 3

39. Highest score first

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.

orderpairscore (key)
1('Cy', 95)95
2('Ana', 88)88
3('Bo', 72)72

40. key can be any function - even a built-in

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.

41. Restore the missing line: Sort by abs, and 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.

42. Sort by abs, and case-insensitively

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.

callresult
sorted(nums, key=abs)[1, -2, -3, 4]
sorted(words)['Apple', 'banana', 'cherry']
sorted(words, key=str.lower)['Apple', 'banana', 'cherry']

43. sorted is stable

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.

44. Ties keep their original 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).

itemkey p[1]final position
('b', 1)11
('d', 1)12
('a', 2)23
('c', 2)24

45. Which is which, by key p[1]

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.

1
('b', 1); ('d', 1)
2
('a', 2); ('c', 2)
g1
key p[1] is "1" for ('b', 1), ('d', 1) — that is what the table on "Ties keep their original order" records, and it is the single property separating this group from the rest.
g2
key p[1] is "2" for ('a', 2), ('c', 2) — that is what the table on "Ties keep their original order" records, and it is the single property separating this group from the rest.

46. max and min take key too

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.

47. The oldest person

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).

nameage (key)picked?
Ana30yes
Bo19no

48. When the key logic grows, use a def

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=.

49. What has to happen first: A named key function

Ranking

Put in order

Put the moves of A named key function into the order they have to happen.

  1. The key returns a tuple: wins, then fewer losses
  2. Pass the name - no parentheses
  3. Read the ranking

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).

50. A named key function

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.

playerwinslosseskey tuple
C52(5, -2)
A31(3, -1)
B34(3, -4)

51. Watch it run: A named key function

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?

  1. Step 1: player is C
  2. Step 2: player is A
  3. Step 3: player is B

52. map: Transform Each Item

Section

Part 4

53. map applies a function to every item

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().

54. Term to definition: Session 29 - Lambdas & Functional Tools

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. first-class value
  • t2. lambda
  • t3. lazy iterator
  • d1. Something you can name, store, pass as an argument, and return. In Python, functions are first-class - they are ordinary values.
  • d2. An anonymous function written inline: lambda parameters: expression. The expression's value is returned automatically - no return keyword.
  • d3. 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().

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.

55. Plan first: A map object is not a list

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:

  1. map returns a lazy object, not the answers
  2. Printing it shows the object, not the values
  3. list() forces it to produce the values

56. A map object is not a list

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].

expressionprints
result<map object at 0x...>
list(result)[10, 20, 30, 40]

57. map is a recipe, list() is the cooking

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.

58. map used directly in a loop

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.

itemstr(item)in joined string
1'1'1
2'2'2
3'3'3

59. filter: Keep Some Items

Section

Part 5

60. filter keeps items where the test is True

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().

61. Teach it back: filter keeps items where the test is True

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().

62. Predict the next row: Keep the big numbers

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

nn > 10kept?
5Falseno
12Trueyes
8Falseno
130Trueyes
44Trueyes

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...>.

63. Keep the big numbers

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].

nn > 10kept?
5Falseno
12Trueyes
8Falseno
130Trueyes
44Trueyes

64. Watch it run: Keep the big numbers

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?

  1. Step 1: n is 5
  2. Step 2: n is 12
  3. Step 3: n is 8
  4. Step 4: n is 130
  5. Step 5: n is 44

65. Restore the missing line: filter with a named test

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.

66. filter with a named test

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].

approachresult
list(filter(is_even, nums))[2, 4, 6]
[n for n in nums if n % 2 == 0][2, 4, 6]

67. Fill in: result for filter with a named test

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.

approachresult
list(filter(is_even, nums))[2, 4, 6]
[n for n in nums if n % 2 == 0][2, 4, 6]

68. Why a Comprehension Wins

Section

Part 6

69. A comprehension does map and filter at once

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.

70. By analogy: A comprehension does map and filter at once

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.

71. Plan first: map vs comprehension

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:

  1. map needs a lambda and a list() wrapper
  2. The comprehension reads left to right
  3. Same result either way

72. map vs comprehension

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.

namevalue
doubled_a[2, 4, 6, 8]
doubled_b[2, 4, 6, 8]
doubled_a == doubled_bTrue

73. What has to happen first: map + filter vs one comprehension

Ranking

Put in order

Put the moves of map + filter vs one comprehension into the order they have to happen.

  1. Nested map/filter is hard to read
  2. The comprehension states it plainly
  3. Identical output

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.

74. map + filter vs one comprehension

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].

approachresult
list(map(..., filter(...)))[4, 16, 36]
[n*n for n in nums if n%2==0][4, 16, 36]

75. Draw the shape of it: map + filter vs one comprehension

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.

76. Reach for the clearest reader

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.

77. Break it if you can: Reach for the clearest reader

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.

78. Traps

Section

Part 7

79. Something is wrong here: a lazy iterator is one-shot

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 [].

80. Trap: a lazy iterator is one-shot

Trap

The 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 [].

namevalue
first[2, 3, 4]
second[]

The fix

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.

namevalue
first[2, 3, 4]
second[2, 3, 4]

81. Something is wrong here: forgetting list() around map

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.

82. Trap: forgetting list() around map

Trap

The 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 writeresult
print(m)<map object at 0x...>
m[0]TypeError: 'map' object is not subscriptable

The fix

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 writeprints
m[10, 20, 30]
m[0]10

83. Break it on purpose: forgetting list() around map

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.

84. Something is wrong here: cramming logic into a lambda

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.

85. Trap: cramming logic into a lambda

Trap

The 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.

playerkey tuplerank
A(3, -1)1
B(3, -4)2

The fix

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.

playerrank_key(p)rank
A(3, -1)1
B(3, -4)2

86. Inspect it line by line: Trap: cramming logic into a lambda

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.

  • Verified by execution: ['A', 'B']. The output is right, yet the tie-break rule is buried in a hard-to-name lambda.
  • 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.

87. Something is wrong here: the old cmp= habit

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.

88. Trap: the old cmp= habit

Trap

The 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 writeresult
sorted(data, cmp=...)TypeError: 'cmp' is an invalid keyword argument for sort()

The fix

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 writeprints
sorted(data, key=lambda n: n)[1, 2, 3]

89. Something is wrong here: key=len() instead of key=len

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.

90. Trap: key=len() instead of key=len

Trap

The 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 writeresult
key=len()TypeError: len() takes exactly one argument (0 given)

The fix

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 writeprints
key=len['hi', 'hey']

91. Which of these survive contact with Session 29 - Lambdas & Functional Tools?

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.

Holds up
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.; 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.
Breaks
Reusing a map object after it is already spent.; Printing or indexing the map object directly.
sound
These are stated as this lesson states them — each one survives the edge cases Session 29 - Lambdas & Functional Tools puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

92. Patterns & Checks

Section

Part 8

93. Sort by a computed key

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).

94. Transform or select a list

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.

95. Where this shows up: Session 29 - Lambdas & Functional Tools

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().

96. Check: sort by length

Check

What order comes out?

words = ["pear", "fig", "banana", "kiwi"]
print(sorted(words, key=len))
wordlen
pear4
fig3
banana6
kiwi4

Check your understanding

What does this print?

  • A. ['fig', 'pear', 'kiwi', 'banana'] (correct)
  • B. ['banana', 'fig', 'kiwi', 'pear']
  • C. ['banana', 'pear', 'kiwi', 'fig']
  • D. ['fig', 'kiwi', 'pear', 'banana']

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.

Why B tempts people
That is alphabetical order (no key), not sorted by length.
Why C tempts people
That is length descending - this call has no reverse=True.
Why D tempts people
pear and kiwi both have length 4; a stable sort keeps pear before kiwi, since pear comes first in the input.

97. Which is which, by len

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.

4
pear; kiwi
3
fig
6
banana
g1
len is "4" for pear, kiwi — that is what the table on "Check: sort by length" records, and it is the single property separating this group from the rest.
g2
len is "3" for fig — that is what the table on "Check: sort by length" records, and it is the single property separating this group from the rest.
g3
len is "6" for banana — that is what the table on "Check: sort by length" records, and it is the single property separating this group from the rest.

98. Check: printing a map

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?

  • A. <map object at 0x...> (correct)
  • B. [10, 20, 30, 40]
  • C. [1, 2, 3, 4]
  • D. map(<lambda>, [1, 2, 3, 4])

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.

Why B tempts people
That is what list(result) prints - map on its own stays lazy and shows the object.
Why C tempts people
The lambda multiplies by 10, so the values would be 10..40 - but only after list() forces them.
Why D tempts people
Python does not echo the source; a map object prints as <map object at 0x...>.

99. Check: list(map(...))

Check

Now it is wrapped in list().

nums = [3, 1, 2]
print(list(map(lambda n: n * n, nums)))
nn * n
39
11
24

Check your understanding

What does this print?

  • A. [9, 1, 4] (correct)
  • B. [1, 4, 9]
  • C. [6, 2, 4]
  • D. <map object at 0x...>

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.

Why B tempts people
map does not sort; it keeps the input order (3, 1, 2), giving [9, 1, 4].
Why C tempts people
n * n is n squared, not n times 2: 3 squared is 9, not 6.
Why D tempts people
list() forces the values, so you get a real list, not the map object.

100. Check: filter keeps which?

Check

Which items survive?

nums = [3, 1, 2]
print(list(filter(lambda n: n > 1, nums)))
nn > 1kept?
3Trueyes
1Falseno
2Trueyes

Check your understanding

What does this print?

  • A. [3, 2] (correct)
  • B. [True, False, True]
  • C. [1]
  • D. [2, 3]

Answer: A

Why: filter keeps items where n > 1 is True: 3 and 2 (in original order); 1 is dropped. Verified by execution.

Why B tempts people
filter returns the kept items, not the True/False results of the test - that would be map.
Why C tempts people
filter keeps the items where the test is True, not the ones where it is False, so 1 is removed, not kept.
Why D tempts people
filter preserves the original order (3 before 2), so it is [3, 2], not [2, 3].

101. Watch it run: Check: filter keeps which?

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?

  1. Step 1: n is 3
  2. Step 2: n is 1
  3. Step 3: n is 2

102. Check: reading a spent iterator

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)
stepwhat is left in result
after firstnothing
second?

Check your understanding

What does print(second) show?

  • A. [] (correct)
  • B. [2, 3, 4]
  • C. [1, 2, 3]
  • D. TypeError

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.

Why B tempts people
That is what first holds. The iterator is already spent, so second cannot see those values again.
Why C tempts people
The lambda adds 1, and in any case result is empty by the time second reads it.
Why D tempts people
Re-reading a spent iterator is not an error; it simply produces nothing.

103. Check: key= vs cmp=

Check

One of these is not a real argument.

data = [3, 1, 2]
print(sorted(data, cmp=lambda a, b: a - b))
argumentexists in Python 3?
key=yes
cmp=no

Check your understanding

What happens?

  • A. TypeError: 'cmp' is an invalid keyword argument for sort() (correct)
  • B. [1, 2, 3]
  • C. [3, 2, 1]
  • D. [3, 1, 2]

Answer: A

Why: Python 3 removed cmp=; sorting is controlled by key=. Passing cmp= raises TypeError before any sorting. Verified by execution.

Why B tempts people
It would sort to [1, 2, 3] only if the argument were valid - but cmp= is rejected outright.
Why C tempts people
There is no descending here, and more importantly cmp= is not accepted at all.
Why D tempts people
The original list is never returned; the invalid keyword raises an error instead.

104. What each one costs: Check: key= vs cmp=

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.

argumentexists in Python 3?
key=yes
cmp=no

105. Check: pass the function, do not call it

Check

Spot the extra parentheses.

words = ["hi", "hey"]
print(sorted(words, key=len()))
writtenmeaning
key=lenhand sorted the function
key=len()call len now, with nothing

Check your understanding

What happens with key=len()?

  • A. TypeError: len() takes exactly one argument (0 given) (correct)
  • B. ['hi', 'hey']
  • C. ['hey', 'hi']
  • D. It sorts alphabetically

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.

Why B tempts people
That is the result of key=len (no parentheses) - here the () causes a crash first.
Why C tempts people
sorted by length descending would need key=len and reverse=True; this line errors instead.
Why D tempts people
No sorting happens at all - len() raises before sorted can compare anything.

106. Fill in: meaning for Check: pass the function, do not call it

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.

writtenmeaning
key=lenhand sorted the function
key=len()call len now, with nothing

107. Connect it up: Session 29 - Lambdas & Functional Tools

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.

108. What you can do now

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 writeIt means
f = shoutgive the function a second name (no call)
lambda n: n * na 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.

Sources

  1. Python 3 docs - sorted() and the key parameter
  2. Python 3 HOWTO - Sorting (key functions, Sort Stability)
  3. Python 3 docs - map() and filter()
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 29).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108