Python Lambda Functions: Sort Keys, Map, Filter and Reduce

A Python lambda is an inline one-expression function. How key functions sort by any criterion, map and filter versus comprehensions, partial and reduce.

  • Course: Python study plan
  • Module: Functions
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: CPython 3.11

What is a lambda function in Python?

A lambda in Python is an anonymous function written as lambda params: expression, whose body is a single expression that is returned automatically. It cannot contain statements, assignments or loops, though a conditional expression is fine. Lambdas belong inline as arguments, such as sorted(pairs, key=lambda p: p[1]); a function worth naming should be a def, which gives tracebacks a real name.

Lesson

A higher-order function takes a function as an argument or returns one, and Python is full of them: sorted(key=…), max(key=…), map, filter, functools.reduce, functools.partial, and every callback and decorator you will meet. A lambda is the anonymous one-expression function you hand to them when a def would be ceremony. This lesson covers the lambda syntax and its single limitation, the key-function idiom that sorts and selects by any criterion, map/filter and when a comprehension beats them, partial for fixing arguments, reduce for folds, and the operator module that names every operator as a function.

lambda

square = lambda x: x * x          # equivalent to def square(x): return x * x
print(square(4))                  # 16

lambda params: expression builds a function object whose body is a single expression — no statements, no return (the expression's value is returned), no assignment, no loops, no if statement (the conditional expression is fine). It can take defaults, *args and **kwargs like any function. Its __name__ is '<lambda>', which is the reason not to assign one to a name as above: def gives the function a real name for tracebacks and help, at the cost of one more line. A lambda belongs inline, as an argument, where the reader sees it and its use together.

Key functions

Every ordering built-in takes key=, a one-argument function applied to each element to produce the value that is actually compared:

words = ["banana", "Apple", "cherry"]
sorted(words)                         # ['Apple', 'banana', 'cherry'] — uppercase sorts first
sorted(words, key=str.lower)          # ['Apple', 'banana', 'cherry'] — case-insensitive
sorted(words, key=len)                # ['Apple', 'banana', 'cherry'] — by length (stable on ties)
max(words, key=len)                   # 'banana'
sorted(pairs, key=lambda p: (-p[1], p[0]))    # by second value descending, then first ascending

The key is computed once per element (not on every comparison), so an expensive key is fine. Returning a tuple sorts by several criteria in order; negating a numeric component reverses just that criterion; reverse=True reverses the whole order. Any callable works as a key — a built-in (len, str.lower, abs), a lambda, a def, or the operator helpers below.

map and filter

map(f, iterable) applies f to each element lazily; filter(pred, iterable) keeps the elements for which pred is true. Both return iterators, so wrap in list() to see them:

list(map(int, ["1", "2", "3"]))                  # [1, 2, 3]
list(filter(str.isdigit, ["1", "a", "22"]))      # ['1', '22']
list(map(lambda x, y: x + y, [1, 2], [10, 20]))  # [11, 22] — map takes several iterables

map(int, tokens) and filter(None, xs) (drops falsy values) are the idiomatic uses: an existing function applied to everything. The moment you write map(lambda x: …) or filter(lambda x: …), a comprehension says the same thing more readably — [x * 2 for x in xs], [x for x in xs if x > 0] — and can combine both (Module 6). Choose map when the function already exists; choose the comprehension when you would have to write a lambda.

functools.partial

partial(f, *args, **kwargs) returns a new function with some of f's arguments fixed:

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5), cube(2))          # 25 8

int_from_binary = partial(int, base=2)
list(map(int_from_binary, ["101", "11"]))   # [5, 3]

It replaces lambda x: power(x, 2), and unlike the lambda it carries its function and fixed arguments as inspectable attributes (square.func, square.keywords). It is also the cleanest fix for late binding in loops (Module 4 lesson 3), because the argument is bound at the moment partial is called.

functools.reduce

reduce(f, iterable, initial) folds a sequence to one value by applying f(accumulator, element) left to right:

from functools import reduce
import operator

reduce(operator.mul, [1, 2, 3, 4], 1)       # 24 — the product
reduce(lambda a, b: a if len(a) >= len(b) else b, words)   # the longest word (first on ties)

Python deliberately left reduce out of the built-ins: sum, min, max, any, all, math.prod and "".join cover the common folds by name, and a for loop with an accumulator is clearer than a reduce with a lambda. Reach for it when the combining function is a named function and the fold has no built-in.

The operator module

operator.add, mul, neg, lt, eq, contains, getitem and the rest are the operators as functions — the arguments reduce, map and sorted want when a lambda would only wrap an operator. Three factories are used constantly with key=:

from operator import itemgetter, attrgetter, methodcaller

sorted(rows, key=itemgetter(2))          # by column 2
sorted(rows, key=itemgetter(1, 0))       # by column 1, then 0 — returns a tuple
sorted(people, key=attrgetter("age"))    # by an attribute
sorted(words, key=methodcaller("lower")) # by calling a method

itemgetter(1) is lambda x: x[1] with a name and a small speed advantage.

Functions returning functions

The factory pattern — a function that builds and returns a specialised function — is the closure of the previous lesson put to work:

def make_validator(lo, hi):
    def valid(x):
        return lo <= x <= hi
    return valid

in_percent = make_validator(0, 100)
print(in_percent(50), in_percent(150))    # True False

Decorators (Module 16) are exactly this with the function itself as the input.

Pitfalls

  • A lambda that needs a statement — it cannot; write a def.
  • Assigning a lambda to a name instead of using def.
  • Forgetting list() around map/filter and printing <map object at …>; or iterating one twice (it is exhausted after the first pass).
  • key=len() — calling instead of passing; key=len.
  • reduce with a lambda where sum or a loop is clearer.
  • sorted(xs, key=lambda x: -x) on strings — negation needs numbers; use reverse=True.

Key takeaways

  • lambda params: expression is an inline single-expression function; use def for anything named or multi-line.
  • key= on sorted/min/max selects the comparison value; tuples sort by several criteria, negation reverses one.
  • map/filter apply existing functions lazily; prefer a comprehension when you would write a lambda.
  • partial fixes arguments; reduce folds with a named function; operator names the operators and itemgetter/attrgetter make keys.
  • A function returning a function is a closure factory — the shape decorators build on.

Common questions

How do I sort a list with a lambda in Python?

Pass it as the key: sorted(pairs, key=lambda p: p[1]) sorts by each element's second item. Return a tuple to sort by several criteria in order, and negate a numeric part to reverse just that one: key=lambda p: (-p[1], p[0]) sorts by the second value descending, then the first ascending.

Should I use map and filter or a list comprehension in Python?

Use map or filter when the function already exists, as in map(int, tokens) or filter(None, xs). The moment you would write a lambda, a comprehension is clearer: [x * 2 for x in xs] and [x for x in xs if x > 0]. map and filter return lazy iterators, so wrap them in list() to see the values.

What does functools.partial do in Python?

partial(f, *args, **kwargs) returns a new function with some of f's arguments fixed, as in square = partial(power, exponent=2) or partial(int, base=2). It replaces a lambda that only wraps a call, keeps its function and arguments inspectable, and binds values immediately, which fixes late binding in loops.

What does reduce do in Python?

functools.reduce(f, iterable, initial) folds a sequence into one value by applying f(accumulator, element) from left to right: reduce(operator.mul, [1, 2, 3, 4], 1) is 24. It is not a built-in on purpose — sum, min, max, math.prod and join cover the common folds, and a loop is often clearer.

Exercises

Sort by several keys

Read n records name score time and sort them by score descending, then by time ascending, then by name ascending, using a single sorted call with a key function returning a tuple. Then print the best record by the same ordering using min with the same key.

Input: n, then n lines. Output: the sorted records as name score time, then best: <name>.

3
ada 90 12
bob 90 10
cy 80 5

prints

bob 90 10
ada 90 12
cy 80 5
best: bob

A pipeline from partial

The first line lists operations as name value pairs: add k, sub k (subtract k), mul k, pow k. Build each step as a functools.partial of the matching two-argument helper with k fixed, then apply the steps in order to every integer on the second line using map and print the results.

Input: a line of operation pairs (possibly empty), then a line of integers. Output: the results, space-separated.

add 3 mul 2
1 2 3

prints

8 10 12

In this module: Functions

← Recursion — base cases, the call stack and memoisation · Type hints and docstrings — the contract a function publishes →