🔵 Intermediate

Lambda, map, filter, reduce

📖 Lesson 17 ⏱ 35 min 🧪 5 questions 💻 3 exercises

🎯 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() and key=
  • 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

Lambdadef
NameAnonymous (no name by default)Named
BodySingle expression onlyAny number of statements
StatementsNo if/for/return etc.Unrestricted
DocstringNot possibleSupported
Best forShort, inline callbacksEverything else
Avoid assigning a lambda to a name (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
SituationPrefer
Transform with a lambdaList comprehension
Filter with a lambdaList comprehension with if
Apply a named function to each itemmap(func, iterable)
Accumulate into a single valuesum(), max(), or a loop; reduce() for custom ops
Sort by a field or computed keysorted(key=…)
Pre-fill function argumentsfunctools.partial()
🤖

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

01 Data Pipeline

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]
02 Custom Sorter

Sort the following list of file paths by:

  1. File extension (alphabetically)
  2. 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
03 Function Dispatcher

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'