🎯 Learning Objectives
- Write and use lambda (anonymous) functions
- Transform iterables with
map() - Filter iterables with
filter() - Accumulate values with
functools.reduce() - Sort with custom keys using
sorted()andkey= - Understand higher-order functions and when to use them vs comprehensions
What is a Lambda?
A lambda is an anonymous, single-expression function.
It is defined inline without a def statement and is most useful
when you need a short, throwaway function — typically as an argument to another function.
# Syntax: lambda parameters : expression
double = lambda x: x * 2
add = lambda x, y: x + y
square = lambda n: n ** 2
print(double(5)) # 10
print(add(3, 4)) # 7
print(square(9)) # 81
lambda_basics.py
Lambda vs def
| Lambda | def | |
|---|---|---|
| Name | Anonymous (no name by default) | Named |
| Body | Single expression only | Any number of statements |
| Statements | No if/for/return etc. | Unrestricted |
| Docstring | Not possible | Supported |
| Best for | Short, inline callbacks | Everything else |
f = lambda x: x*2).
PEP 8 says: if you need a named function, use def.
Lambdas exist for passing functions inline — not as a replacement for def.
map()
map(function, iterable) applies a function to every item in an iterable
and returns a lazy iterator of results:
numbers = [1, 2, 3, 4, 5]
# Apply a named function
def square(n):
return n ** 2
result = list(map(square, numbers))
print(result) # [1, 4, 9, 16, 25]
# Apply a lambda inline
result = list(map(lambda n: n ** 2, numbers))
print(result) # [1, 4, 9, 16, 25]
# Equivalent list comprehension (often preferred)
result = [n ** 2 for n in numbers]
map_basics.py
map() with multiple iterables
xs = [1, 2, 3, 4]
ys = [10, 20, 30, 40]
# Function is called with one element from each iterable
sums = list(map(lambda x, y: x + y, xs, ys))
print(sums) # [11, 22, 33, 44]
# Also works with a named function
import operator
products = list(map(operator.mul, xs, ys))
print(products) # [10, 40, 90, 160]
map_multi.py
map() returns a lazy iterator — wrap it in list() only
when you actually need all results at once. If you're just iterating once (e.g.
passing to sum()), skip the list() call.
filter()
filter(function, iterable) returns a lazy iterator of the items for
which the function returns a truthy value:
numbers = range(-5, 6) # -5, -4, ..., 5
positives = list(filter(lambda n: n > 0, numbers))
print(positives) # [1, 2, 3, 4, 5]
# filter(None, iterable) — removes falsy values
mixed = [0, 1, "", "hello", None, 42, False, True]
truthy = list(filter(None, mixed))
print(truthy) # [1, 'hello', 42, True]
# Equivalent comprehension
truthy = [x for x in mixed if x]
filter_basics.py
words = ["apple", "Banana", "cherry", "Date", "elderberry"]
# Keep only lowercase-starting words
lower_words = list(filter(lambda w: w[0].islower(), words))
print(lower_words) # ['apple', 'cherry', 'elderberry']
# Using a named predicate for clarity
def is_long(word):
return len(word) > 5
long_words = list(filter(is_long, words))
print(long_words) # ['Banana', 'cherry', 'elderberry']
filter_strings.py
reduce()
functools.reduce(function, iterable[, initializer]) accumulates a
sequence into a single value by repeatedly applying a two-argument function:
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# Sum — applies: ((((1+2)+3)+4)+5) = 15
total = reduce(lambda acc, x: acc + x, numbers)
print(total) # 15
# Product
product = reduce(lambda acc, x: acc * x, numbers)
print(product) # 120
# Maximum value (for illustration — use max() in practice)
maximum = reduce(lambda a, b: a if a > b else b, numbers)
print(maximum) # 5
reduce_basics.py
Using an initializer
from functools import reduce
# Initializer is the starting accumulator value
# Useful for empty iterables or to set a starting point
total = reduce(lambda acc, x: acc + x, [], 0) # empty list — returns 0
print(total) # 0
# Build a dict from a list of key-value pairs
pairs = [("a", 1), ("b", 2), ("c", 3)]
result = reduce(lambda d, kv: {**d, kv[0]: kv[1]}, pairs, {})
print(result) # {'a': 1, 'b': 2, 'c': 3}
# In practice, dict() or a comprehension is cleaner:
result = dict(pairs)
reduce_init.py
reduce() was a built-in in Python 2 but moved to functools
in Python 3 — a deliberate signal that it's not the go-to tool. For most accumulation
tasks, a loop, sum(), max(), or a comprehension is clearer.
reduce() shines for genuinely recursive accumulation over arbitrary
binary operations.
sorted() with key=
The built-in sorted() (and list.sort()) accept a
key= argument — a function called once per item to produce a
comparison value:
words = ["banana", "Apple", "cherry", "date"]
# Case-insensitive sort
print(sorted(words, key=str.lower))
# ['Apple', 'banana', 'cherry', 'date']
# Sort by length
print(sorted(words, key=len))
# ['date', 'Apple', 'banana', 'cherry']
# Sort numbers by absolute value
nums = [-4, 2, -1, 3, -5]
print(sorted(nums, key=abs))
# [-1, 2, -4, 3, -5]
# Reverse sort
print(sorted(nums, key=abs, reverse=True))
# [-5, -4, 3, 2, -1]
sorted_key.py
Multi-key sorting
employees = [
{"name": "Alice", "dept": "Engineering", "salary": 95000},
{"name": "Bob", "dept": "Marketing", "salary": 78000},
{"name": "Carol", "dept": "Engineering", "salary": 102000},
{"name": "Dave", "dept": "Marketing", "salary": 85000},
]
# Sort by department, then by salary descending within each department
sorted_emp = sorted(
employees,
key=lambda e: (e["dept"], -e["salary"])
)
for e in sorted_emp:
print(f"{e['dept']:15} {e['name']:8} £{e['salary']:,}")
# Engineering Carol £102,000
# Engineering Alice £95,000
# Marketing Dave £85,000
# Marketing Bob £78,000
multi_key_sort.py
operator module helpers
import operator
# operator.itemgetter — faster than a lambda for dict/tuple access
sorted_emp = sorted(employees, key=operator.itemgetter("salary"))
# operator.attrgetter — for sorting objects by attribute
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
points = [Point(3, 1), Point(1, 4), Point(2, 2)]
by_x = sorted(points, key=operator.attrgetter("x"))
print(by_x) # [Point(x=1,y=4), Point(x=2,y=2), Point(x=3,y=1)]
operator_helpers.py
Higher-Order Functions
A higher-order function is one that either:
- Takes a function as an argument (
map,filter,sorted), or - Returns a function as its result (function factories, decorators)
# Returning a function — function factory
def make_multiplier(factor):
return lambda x: x * factor
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Accepting a function as an argument
def apply_twice(func, value):
return func(func(value))
print(apply_twice(double, 3)) # 12 (3→6→12)
print(apply_twice(str.upper, "hello")) # TypeError — str.upper takes no args directly
# Composing functions
def compose(f, g):
"""Returns a new function that applies g then f."""
return lambda x: f(g(x))
add_one = lambda x: x + 1
square = lambda x: x ** 2
square_then_add = compose(add_one, square)
print(square_then_add(4)) # (4²)+1 = 17
higher_order.py
functools.partial
partial(func, *args, **kwargs) creates a new function with some
arguments pre-filled — a technique called partial application:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(4)) # 16
print(cube(3)) # 27
# Practical: pre-fill print's sep and end
csv_print = partial(print, sep=",", end="\n")
csv_print("Alice", 30, "Engineering") # Alice,30,Engineering
# Pre-fill a log level
import logging
warn = partial(logging.log, logging.WARNING)
warn("disk space low")
partial.py
Real-World Patterns
Data pipeline with map + filter
raw_data = [
" Alice, 30, Engineer ",
"Bob, 25, Designer",
" carol, 35, Manager",
"", # blank — should be skipped
"Dave, -1, Analyst", # invalid age
]
def parse_record(line):
parts = [p.strip() for p in line.split(",")]
return {"name": parts[0].title(), "age": int(parts[1]), "role": parts[2]}
def is_valid(record):
return record["age"] >= 0
# Pipeline: clean → parse → filter
records = list(
filter(is_valid,
map(parse_record,
filter(None, (line.strip() for line in raw_data))))
)
for r in records:
print(r)
# {'name': 'Alice', 'age': 30, 'role': 'Engineer'}
# {'name': 'Bob', 'age': 25, 'role': 'Designer'}
# {'name': 'Carol', 'age': 35, 'role': 'Manager'}
pipeline.py
Dispatch table
# Map operation names to functions — avoids long if/elif chains
import operator
OPERATIONS = {
"add": operator.add,
"subtract": operator.sub,
"multiply": operator.mul,
"divide": operator.truediv,
}
def calculate(op_name, a, b):
op = OPERATIONS.get(op_name)
if op is None:
raise ValueError(f"Unknown operation: {op_name}")
return op(a, b)
print(calculate("add", 10, 3)) # 13
print(calculate("multiply", 4, 5)) # 20
dispatch.py
map / filter vs Comprehensions
In modern Python, comprehensions are usually preferred over
map() and filter() with lambdas because they're more readable:
data = range(10)
# ── map + lambda ──
result = list(map(lambda x: x ** 2, data))
# ── Equivalent comprehension — cleaner ──
result = [x ** 2 for x in data]
# ── filter + lambda ──
evens = list(filter(lambda x: x % 2 == 0, data))
# ── Equivalent comprehension ──
evens = [x for x in data if x % 2 == 0]
# ── When map() WINS — using a named function (no lambda needed) ──
names = ["alice", "bob", "carol"]
upper = list(map(str.upper, names)) # clean, no lambda
# vs
upper = [name.upper() for name in names] # also fine
map_vs_comprehension.py
| Situation | Prefer |
|---|---|
| Transform with a lambda | List comprehension |
| Filter with a lambda | List comprehension with if |
| Apply a named function to each item | map(func, iterable) |
| Accumulate into a single value | sum(), max(), or a loop; reduce() for custom ops |
| Sort by a field or computed key | sorted(key=…) |
| Pre-fill function arguments | functools.partial() |
Primary sources: Python Docs — map() · Python Docs — filter() · Python Docs — functools · Python Docs — Sorting HowTo
Ask your AI tutor! Not sure whether to use map()
or a comprehension for a specific task? Struggling with a multi-key sort?
Want to see a functional-style pipeline refactored step by step? Ask away.
💻 Exercises
Given a list of raw score strings, build a pipeline using
map(), filter(), and sorted()
(no explicit loops) to produce a sorted list of valid float scores
that are between 0 and 100 inclusive.
raw = ["85.5", "102", "NaN", "67.0", "-3", "99.9", "abc", "50"]
Expected output: [50.0, 67.0, 85.5, 99.9]
Show solution
def try_float(s):
try:
return float(s)
except ValueError:
return None
raw = ["85.5", "102", "NaN", "67.0", "-3", "99.9", "abc", "50"]
# Step 1: parse (None for invalid)
parsed = map(try_float, raw)
# Step 2: remove None and out-of-range values
valid = filter(lambda x: x is not None and 0 <= x <= 100, parsed)
# Step 3: sort
result = sorted(valid)
print(result) # [50.0, 67.0, 85.5, 99.9]
Sort the following list of file paths by:
- File extension (alphabetically)
- Then by filename (case-insensitive) within the same extension
files = [
"report.pdf", "notes.txt", "photo.jpg",
"README.txt", "archive.zip", "budget.pdf",
"image.jpg", "data.csv"
]
Show solution
files = [
"report.pdf", "notes.txt", "photo.jpg",
"README.txt", "archive.zip", "budget.pdf",
"image.jpg", "data.csv"
]
def sort_key(filename):
name, _, ext = filename.rpartition(".")
return (ext.lower(), name.lower())
sorted_files = sorted(files, key=sort_key)
for f in sorted_files:
print(f)
# data.csv
# image.jpg
# photo.jpg
# budget.pdf
# report.pdf
# notes.txt
# README.txt
# archive.zip
Build a text-processing dispatcher that maps command names to functions:
"upper"→ convert to upper-case"lower"→ convert to lower-case"reverse"→ reverse the string"title"→ title-case"length"→ return the length as a string
Write a function process(text, *commands) that applies each
command in order using the dispatch table (not if/elif).
Raise ValueError for unknown commands.
Show solution
COMMANDS = {
"upper": str.upper,
"lower": str.lower,
"reverse": lambda s: s[::-1],
"title": str.title,
"length": lambda s: str(len(s)),
}
def process(text, *commands):
result = text
for cmd in commands:
if cmd not in COMMANDS:
raise ValueError(f"Unknown command: '{cmd}'")
result = COMMANDS[cmd](result)
return result
print(process("hello world", "upper")) # HELLO WORLD
print(process("Hello World", "reverse")) # dlroW olleH
print(process("hello", "upper", "reverse")) # OLLEH
print(process(" python ", "title", "length")) # 9
try:
process("hi", "shout")
except ValueError as e:
print(e) # Unknown command: 'shout'