Module 4 · Functions & Modularity ⏱ 16 min

Anonymous (Lambda) Functions

By the end of this lesson you will be able to:
  • Write a lambda as a short, single-expression function with no name
  • Pass a lambda inline as a key to sorted, map, and filter
  • Explain the late-binding trap and capture a value with a default argument
  • Recognize when a lambda is too clever and a def would be clearer

A lambda is a tiny anonymous function — a function with no name, written as a single expression. Use it when you need a quick function right here, right now, especially as an argument you hand to another function:

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

Read it as lambda parameters: expression. The expression is evaluated and returned automatically — there is no return keyword, because there is nowhere to put one. That lambda is exactly equivalent to the definition below; the only thing it lacks is a name:

def square(x):
    return x * x

Anything you can do with a lambda you can do with a def. Lambdas exist for brevity: for a one-line transformation passed straight into another function, a named definition can feel like needless ceremony.

flowchart LR
  I["[1, 2, 3]"] --> L["lambda n: n * 2"] --> O["[2, 4, 6]"]
  style L fill:#3776ab,color:#fff
A lambda applies one expression to each input — perfect for map.

Inline arguments

Lambdas shine as inline arguments. The classic case is a key for sorted(), where you only need the function once:

names = ["Bo", "Alexandra", "Cy"]
by_length = sorted(names, key=lambda name: len(name))
print(by_length)   # ['Bo', 'Cy', 'Alexandra']

map and filter also take a function and a sequence, so a lambda slots right in:

nums = [1, 2, 3, 4]
doubled = list(map(lambda n: n * 2, nums))         # [2, 4, 6, 8]
evens = list(filter(lambda n: n % 2 == 0, nums))   # [2, 4]

Often a list comprehension does the same job more clearly — [n * 2 for n in nums]. Reach for a comprehension when it reads better (see the Comprehensions lesson).

A lambda as a sort key, and as the function for map and filter. Press Run.
names = ["Bo", "Alexandra", "Cy"]
print(sorted(names, key=lambda name: len(name)))

nums = [1, 2, 3, 4]
print(list(map(lambda n: n * 2, nums)))
print(list(filter(lambda n: n % 2 == 0, nums)))

combine = lambda x, y: x + y
print(combine(3, 4))

Lambdas capture their surroundings

A lambda can use variables from the code around it, not just its own parameters. When you write lambda x: x * factor, the name factor is captured from the enclosing scope and remembered for later. That turns a lambda into a tiny factory: a function that builds and returns a lambda customized with a value.

Capturing has one famous sharp edge inside loops. The list [lambda: i for i in range(3)] builds three lambdas, but every one of them reads the same variable i. They do not snapshot the value — they look the name up later, by which point the loop has finished and i is 2. So all three return 2, never 0, 1, 2. The figure below shows why.

flowchart LR
  F1["fn 0"] --> I["loop variable i"]
  F2["fn 1"] --> I
  F3["fn 2"] --> I
  I --> R["i is 2 by the time any fn runs"]
  style I fill:#b45309,color:#fff
  style R fill:#3776ab,color:#fff
Late binding: three lambdas all read the same loop variable, which ends at 2.
The late-binding bug, and the default-argument fix that snapshots each value.
# Each lambda reads i LATER, when i is already 2.
buggy = [lambda: i for i in range(3)]
print([f() for f in buggy])     # [2, 2, 2]

# A default argument captures i's value at definition time.
fixed = [lambda i=i: i for i in range(3)]
print([f() for f in fixed])     # [0, 1, 2]

When a def is the better choice

A lambda is a single expression with no name, no statements, and no return. The moment your logic needs more than that — a conditional with several branches, a helper variable, a loop, or a docstring — reach for a real def. The name itself is documentation: def distance(p1, p2) tells the next reader what it does, while a bare lambda a, b: ... forces them to reverse-engineer the intent.

A useful habit is to keep lambdas short and local. If you assign one to a meaningful name and never pass it anywhere, that is usually a def in disguise — write it that way and your meaning becomes obvious to everyone who follows.

Exercise

Write make_multiplier(factor) that returns a lambda which multiplies its input by factor. So make_multiplier(3) returns a function, and calling that function on 10 gives 30.

def make_multiplier(factor):
    # return lambda x: ...
    pass
Exercise

Write sort_by_last(words) that returns words sorted by each word's last character. Use a lambda as the key.

def sort_by_last(words):
    # return sorted(words, key=lambda ...)
    pass
Exercise

What does this print? A lambda doubles each number via map.

nums = [1, 2, 3]
doubled = list(map(lambda n: n * 2, nums))
print(doubled)
Exercise

This lambda won't even parse — return is illegal inside a lambda body. Fix it so sign(n) returns "pos" when n >= 0 and "neg" otherwise. Use a single conditional expression (a if cond else b).

sign = lambda n: return "pos" if n >= 0 else "neg"
Exercise

Write make_counters() that returns a list of three zero-argument functions — the first returns 0, the second 1, the third 2. A naive [lambda: i for i in range(3)] has the late-binding bug (all return 2); capture each value with a default argument.

def make_counters():
    # return [lambda ... for i in range(3)]  -- bind i so each fn keeps its own value
    pass

The ternary: a conditional as one expression

Because a lambda body is a single expression, you cannot write an if statement inside it. You can still make choices, using Python's conditional expression — often called the ternary. It reads as value_if_true if condition else value_if_false, and the whole thing is one expression with a value:

parity = lambda n: 'even' if n % 2 == 0 else 'odd'
print(parity(4))   # even

The ternary evaluates the condition, then yields exactly one of the two branches. It works anywhere an expression is allowed — inside a return, inside a list, and inside a lambda. Reserve it for genuinely two-way choices; a long chain of nested ternaries quickly becomes harder to read than a plain if.

Sorting on more than one key

A key function can return any value Python knows how to compare, including a tuple. Return a tuple and sorted() compares element by element — first by the first element, then by the second to break ties. That is how you sort on more than one criterion with a single lambda:

people = [('Ada', 90), ('Lin', 90), ('Bo', 70)]
order = sorted(people, key=lambda p: (-p[1], p[0]))
print(order)   # [('Ada', 90), ('Lin', 90), ('Bo', 70)]

The minus sign on the score sorts it descending while the name breaks ties ascending. Tuples compared this way are the standard trick for multi-key sorts, and a lambda is the cleanest way to express the key.

Lambdas are a convenience, not a requirement

It is worth saying plainly: you can write entire, excellent programs without a single lambda. Every lambda has an exact def equivalent, and the def is often more readable because it carries a name and a docstring. Lambdas earn their place in two spots — as a short key or callback handed straight into another function, and in quick interactive experiments where typing a full definition breaks your flow.

If you ever feel a lambda is the only way to solve a problem, that feeling is almost always wrong. Reach for the tool that makes the line readable; most of the time, that tool is a named function.

Recap

  • A lambda is lambda params: expression — one expression, returned automatically, with no return.
  • It shines as an inline argument: the key for sorted, or a function for map and filter.
  • Lambdas capture outer variables by name, which causes late-binding bugs inside loops; bind a value with a default argument like lambda i=i: i.
  • When the logic needs statements, branches, or a name, write a def instead.

Next you'll see how comprehensions can replace many map and filter calls with something even more readable.

Checkpoint quiz

Which is a valid lambda that doubles its input?

Why does [lambda: i for i in range(3)] return [2, 2, 2] when called?

When is a named def better than a lambda?

Go deeper — technical resources