Key Functions and Lambda
Python has built-in functions for common data operations: sorted() for ordering, map() for transforming, and filter() for selecting. A lambda is a tiny function written in one line. These tools let you express data transformations concisely.
This lesson covers sorted(key=), min(key=), max(key=), map(), filter(), and lambda expressions. Comprehensions are generally preferred over map/filter in Python, but sorted(key=) with key functions is an essential tool you will use constantly.
James sorts his notes by word count:
notes.sort(key=lambda n: n.word_count, reverse=True)
"Wait," he says. "What is lambda n: n.word_count?"
"A function without a name," Emma says. "You could write a full function for it. But when all you need is 'give me the word count,' a lambda says it in one line."
sorted() with key Functions
You have used sorted() before to sort simple lists like sorted([3, 1, 2]). But how do you sort a list of Note objects? Python does not know whether to sort by title, word count, or author. You tell it by passing a key function: a function that takes one item and returns the value to sort by.
Start with a named function to see how key= works:
from dataclasses import dataclass, field
@dataclass
class Note:
title: str
body: str
word_count: int
author: str = "Anonymous"
is_draft: bool = True
tags: list[str] = field(default_factory=list)
notes = [
Note("Python Tips", "Learn basics", 50, "James", tags=["python"]),
Note("Debug Guide", "Fix errors", 120, "James", tags=["debug"]),
Note("Cooking", "Boil water", 30, "Emma", tags=["cooking"]),
]
def get_word_count(note: Note) -> int:
"""Return the word count of a note (used as a sort key)."""
return note.word_count
# Sort by word count (ascending)
by_length = sorted(notes, key=get_word_count)
for note in by_length:
print(f"{note.title}: {note.word_count}")
Output:
Cooking: 30
Python Tips: 50
Debug Guide: 120
Here is what key=get_word_count does: Python calls get_word_count(note) on each note, gets back a number, and sorts the notes by those numbers. The note with word count 30 comes first, then 50, then 120.
Writing a whole function just to return one attribute is verbose. That is where lambda comes in. A lambda is a one-line anonymous function:
# Sort by title (alphabetical)
by_title = sorted(notes, key=lambda n: n.title)
# Sort by word count (descending)
by_length_desc = sorted(notes, key=lambda n: n.word_count, reverse=True)
# Sort by number of tags
by_tags = sorted(notes, key=lambda n: len(n.tags))
The same key pattern works with min() and max():
shortest = min(notes, key=lambda n: n.word_count)
longest = max(notes, key=lambda n: n.word_count)
print(f"Shortest: {shortest.title} ({shortest.word_count} words)")
print(f"Longest: {longest.title} ({longest.word_count} words)")
Output:
Shortest: Cooking (30 words)
Longest: Debug Guide (120 words)
Lambda Expressions
A lambda is an anonymous function: a function without a name. It is written in one line:
# Named function
def get_word_count(note: Note) -> int:
return note.word_count
# Lambda equivalent
get_word_count = lambda note: note.word_count
Both do the same thing. The lambda syntax is lambda parameters: expression. No def, no return, no name. The example above assigns a lambda to a variable for comparison only; in practice, PEP 8 discourages naming lambdas (use def instead). Lambdas shine as inline arguments to sorted(), min(), and max().
| Named function | Lambda | When to use |
|---|---|---|
def get_wc(n): return n.word_count | lambda n: n.word_count | Simple, one-expression operations |
| Multi-line, complex logic | Not possible with lambda | Use named function |
| Reused in multiple places | Used once inline | Named for reuse, lambda for one-off |
Rule of thumb: if the lambda is longer than one readable line, use a named function instead. Lambdas are for simple key functions, not for complex logic.
# 🟢 Good lambda: simple attribute access
sorted(notes, key=lambda n: n.word_count)
# 🔴 Bad lambda: too complex
sorted(notes, key=lambda n: n.word_count if not n.is_draft else -n.word_count)
# 🟢 Better as a named function:
def sort_key(note: Note) -> int:
if note.is_draft:
return -note.word_count
return note.word_count
sorted(notes, key=sort_key)
map() and filter()
You already know how to transform and filter lists with comprehensions. So why learn map() and filter()? Two reasons: you will see them in other people's code, and they work well when you already have a named function to apply. They are not better than comprehensions; they are an alternative you should recognize.
map() applies a function to every item in a list. filter() keeps only items that pass a test. Both return lazy iterators (like generators from Lesson 3), not lists. You wrap them in list() to get a list:
# map: apply a function to every item, get titles
titles = list(map(lambda n: n.title, notes))
print(titles)
# filter: keep only items where the function returns True
published = list(filter(lambda n: not n.is_draft, notes))
for note in published:
print(note.title)
Output:
['Python Tips', 'Debug Guide', 'Cooking']
Debug Guide
Cooking
Why list(map(...))? Because map() returns a lazy iterator that produces values one at a time (just like a generator). Wrapping it in list() collects all the values into a list.
In Python, comprehensions are usually preferred over map() and filter():
# map equivalent
titles = [n.title for n in notes]
# filter equivalent
published = [n for n in notes if not n.is_draft]
The comprehension versions are more readable. Use map() and filter() when you have an existing named function:
def is_published(note: Note) -> bool:
return not note.is_draft
# filter with named function reads well
published = list(filter(is_published, notes))
PRIMM-AI+ Practice: Predict the Sort
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode.
data = [("Charlie", 85), ("Alice", 92), ("Bob", 78), ("Diana", 92)]
result_a = sorted(data, key=lambda x: x[1])
result_b = sorted(data, key=lambda x: x[1], reverse=True)
result_c = sorted(data, key=lambda x: (-x[1], x[0]))
For each result, what is the first and last item?
Check your predictions
result_a = [('Bob', 78), ('Charlie', 85), ('Alice', 92), ('Diana', 92)]
# First: Bob (78), Last: Diana (92)
result_b = [('Alice', 92), ('Diana', 92), ('Charlie', 85), ('Bob', 78)]
# First: Alice (92), Last: Bob (78)
result_c = [('Alice', 92), ('Diana', 92), ('Charlie', 85), ('Bob', 78)]
# First: Alice (92), Last: Bob (78)
result_c uses a tuple key: sort by score descending (negative), then by name ascending. This puts Alice before Diana because "Alice" < "Diana" alphabetically.
Run
Press Shift+Tab to exit Plan Mode.
Create functional_practice.py and verify your predictions.
Investigate
For result_c from the Predict exercise, write one sentence explaining why Alice appears before Diana even though they have the same score. What does the negative sign in (-x[1], x[0]) do?
Modify
Rewrite your exclude_drafts function from Chapter 62 as a filter() call with a lambda. Then rewrite it back as a comprehension. Which is more readable?
Make [Mastery Gate]
Write a function sort_notes(notes: list[Note], by: str = "word_count", reverse: bool = False) -> list[Note] that sorts notes by the given attribute. Use sorted() with a key function. In Claude Code, type /tdg to guide you through the cycle:
- Write the stub
- Write 3+ tests (sort by word_count, sort by title, reverse order)
- Prompt AI to implement
- Verify with ruff, pyright, pytest
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Comprehension vs map/filter
When should I use map() and filter() instead of list
comprehensions in Python? Show me three cases where
map/filter is actually better than a comprehension.
What you're learning: The Python community generally prefers comprehensions, but map/filter shine with existing named functions and in certain pipeline patterns. The AI helps you understand the minority cases.
Prompt 2: Advanced Sorting
Show me how to sort a list of Note objects by multiple
criteria: first by author (alphabetical), then by
word_count (descending), then by title (alphabetical).
Use both lambda and operator.attrgetter approaches.
What you're learning: Multi-key sorting is a common production need. The AI shows you the tuple-key pattern and operator.attrgetter, both professional approaches.
Prompt 3: Lambda Readability
Take this sorting expression:
sorted(notes, key=lambda n: (-n.word_count, n.title.lower(), len(n.tags)))
Rewrite it using a named function instead of lambda.
Which version is more readable? At what complexity
should you switch from lambda to a named function?
What you're learning: Lambda readability has a tipping point. Short lambdas (lambda n: n.word_count) are clear. Complex lambdas with multiple expressions become harder to read than a named function. Learning to recognize that threshold is a practical design skill.
James sorts his notes three different ways in three lines. sorted(key=) with a lambda, min() to find the shortest, filter() with a named function to exclude drafts.
"At the warehouse, we had sorting stations," he says. "Sort by destination, sort by size, sort by priority. Each station had its own rule. The key function is the sorting rule."
Emma nods. "Next lesson, you will learn lru_cache and partial from the functools module. Caching remembers expensive results so you do not recompute them. Partial pre-fills arguments to create specialized functions. Both build on the function-as-argument pattern you just learned."