Skip to content
C Codeloom
Python

Lambda Functions in Python: When and Why

A practical guide to Python lambda functions — syntax, use with sorted, map, and filter, common patterns, and the situations where a named def is the clearer choice.

·8 min read · By Yash Kesharwani
Intermediate 9 min read

What you'll learn

  • The lambda syntax and what it returns
  • How lambdas are used with sorted, min, max, map, and filter
  • Where lambdas shine and where they hurt readability
  • Closures — how a lambda captures variables from its surroundings
  • When a named def is the better choice

Prerequisites

A lambda is a small anonymous function written as an expression. It takes zero or more arguments, evaluates a single expression, and returns the result. Lambdas are everywhere in real Python code — passed as sort keys, plugged into map and filter, used as quick callbacks — but they are also one of the easiest features to overuse. This post covers the syntax, the natural uses, and the line past which you should reach for def instead.

The syntax

A lambda has the shape:

lambda <parameters>: <expression>

It is a function — just written as an expression, with no name and no return keyword:

square = lambda n: n * n
print(square(5))    # 25

The body is a single expression. Its value is what the function returns. You cannot put statements (assignments, if blocks, loops) inside a lambda — only an expression.

Compare to the equivalent def:

def square(n):
    return n * n

Functionally identical. The only differences:

  • The lambda has no name unless you assign it to a variable
  • The lambda has no docstring
  • The lambda is restricted to a single expression

Style note: assigning a lambda to a name (square = lambda n: n * n) is generally considered bad form. If you’re going to name it, write def — you get a docstring, a real __name__ in tracebacks, and more room to grow. Lambdas earn their keep precisely when the function is so small and so local that giving it a name is overkill.

The natural habitat: sort keys

By far the most common use of lambda is as the key argument to sorted, min, max, and list.sort:

people = [
    {"name": "Alice", "age": 30},
    {"name": "Bob",   "age": 25},
    {"name": "Carol", "age": 35},
]

by_age = sorted(people, key=lambda p: p["age"])
print(by_age)
# [{'name': 'Bob', 'age': 25}, {'name': 'Alice', 'age': 30}, {'name': 'Carol', 'age': 35}]

The key function is called once per element to produce the value that will actually be compared. Here we’re saying: “for each person, the thing to sort by is their age.”

This pattern reads cleanly because the lambda is short, anonymous, and used once at the call site. Defining a separate def get_age(p): return p["age"] would add a line and a name for no benefit.

Sorting by multiple keys is just as compact — return a tuple:

records = [("alice", 90), ("bob", 90), ("alice", 70)]
records.sort(key=lambda r: (r[0], -r[1]))
print(records)
# [('alice', 90), ('alice', 70), ('bob', 90)]

This sorts by name ascending, then by score descending.

max and min take the same key argument:

words = ["banana", "fig", "blueberry", "kiwi"]
longest = max(words, key=lambda w: len(w))
print(longest)    # blueberry

This particular case can also be written max(words, key=len) — when the key function already exists with a simple name, use it directly. Skip the lambda.

map and filter

map(func, iterable) applies a function to every item. filter(func, iterable) keeps items where the function returns truthy. Both return iterators, which you can wrap in list(...) to materialise.

nums = [1, 2, 3, 4, 5]

squares = list(map(lambda n: n * n, nums))
print(squares)    # [1, 4, 9, 16, 25]

evens = list(filter(lambda n: n % 2 == 0, nums))
print(evens)      # [2, 4]

A frank admission: in modern Python, list comprehensions are usually clearer than map and filter for transformations like these:

squares = [n * n for n in nums]
evens   = [n for n in nums if n % 2 == 0]

See List Comprehensions. The comprehensions read as English and don’t require a lambda at all. map and filter come into their own when the function isn’t a lambda — when you’ve already got a perfectly good named function and you want to apply it across a sequence.

names = ["  alice ", "BOB", "Carol  "]
clean = list(map(str.strip, names))
print(clean)    # ['alice', 'BOB', 'Carol']

That str.strip is a method passed as a value — no lambda needed.

Lambdas inside other callables

Many standard-library and third-party functions take a callback. A lambda is often the lightest way to plug one in:

from functools import reduce

product = reduce(lambda a, b: a * b, [1, 2, 3, 4])
print(product)    # 24

reduce walks through the list applying the function pairwise. The lambda says “combine a and b by multiplying.” For something this small, a lambda is reasonable. For anything more elaborate, def is clearer.

In GUI and async code, callbacks are everywhere. A common pattern is wiring up a button to call a function with a specific argument:

button.on_click(lambda: handle_click(item_id))

The lambda freezes item_id into the call. This is a closure — the lambda captures the surrounding variable.

Try it yourself. Given products = [{"name": "Pen", "price": 2.50}, {"name": "Notebook", "price": 4.00}, {"name": "Eraser", "price": 0.75}], use sorted with a lambda to produce a list of products ordered by price ascending. Then use max with a key lambda to find the most expensive product.

Closures and the late-binding gotcha

A lambda that references a name from its surroundings looks the name up when it runs, not when it’s defined. This is the classic loop trap:

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])    # [2, 2, 2]   — not [0, 1, 2]

Every lambda references the same i. By the time you call them, the loop has finished and i is 2. The fix is to bind the current value at definition time using a default argument:

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])    # [0, 1, 2]

i=i evaluates the right-hand i immediately and stores it as the lambda’s default — see Default and Keyword Arguments for why this works. The same trap exists with regular def inside a loop, but it bites lambda users more often because lambdas are how callbacks are typically wired up.

When def is the better choice

A few clear cases where you should write def instead of lambda:

The function is non-trivial. If you find yourself reaching for and, or, or a conditional expression to cram more logic into a lambda, write a def. Lambdas are for one-line expressions; the moment one wants a second thought, give it a name.

# Awkward
key = lambda r: (r["score"] if r["valid"] else float("-inf"), -len(r["name"]))

# Clearer
def sort_key(r):
    score = r["score"] if r["valid"] else float("-inf")
    return score, -len(r["name"])

You want to reuse it. A named function can be called from anywhere. A lambda buried in a call site is harder to find later.

You want a good traceback. If the function raises, tracebacks show <lambda> instead of a meaningful name. For anything user-facing or debugged in production, a real name is worth the four extra characters.

You’re using map or filter with a trivial transformation. A list comprehension is almost always nicer.

The single argument already names a method or builtin. key=str.lower beats key=lambda s: s.lower(). key=len beats key=lambda x: len(x).

A small worked example

Sorting a list of file records by extension, then by name within each extension group:

files = [
    "report.pdf",
    "notes.txt",
    "image.png",
    "archive.zip",
    "letter.txt",
    "summary.pdf",
]

def key_fn(name):
    base, _, ext = name.rpartition(".")
    return (ext, base)

print(sorted(files, key=key_fn))
# ['report.pdf', 'summary.pdf', 'image.png', 'letter.txt', 'notes.txt', 'archive.zip']

Here a def reads much better than the equivalent one-line lambda:

# Possible — but cramped
sorted(files, key=lambda n: (n.rpartition(".")[2], n.rpartition(".")[0]))

The named version avoids the duplicate rpartition call and explains itself. Use a lambda when its expression really is one short thought.

Try it yourself. Given scores = [("alice", 91), ("bob", 75), ("carol", 91), ("dan", 60)], use sorted with a single lambda to sort by score descending, breaking ties by name ascending. Confirm the result begins with ("alice", 91).

Recap

You now know:

  • A lambda is an expression-only anonymous function: lambda x, y: x + y
  • Its natural home is the key= argument to sorted, min, max, and list.sort
  • map and filter accept lambdas, but list comprehensions usually read better
  • Closures capture variables by reference — bind early with x=x if you need today’s value
  • When a lambda starts to wrinkle, write a def — name it, document it, debug it

Next steps

Lambdas and comprehensions share a common ancestor: the iterator. The next post unpacks Python’s iterator protocol, the yield keyword, and how generators let you describe potentially infinite sequences while using almost no memory.

→ Next: Generators and Iterators in Python

Questions or feedback? Email codeloomdevv@gmail.com.