Python — Lambdas & Functional Tools
Lambda expressions let you write small, anonymous functions inline. Combined with the operator module, functools.partial, and higher-order functions like map, filter, and reduce, they form Python's functional-programming toolkit. Knowing when to reach for a lambda — and when to reach for a comprehension or a named function — is a hallmark of idiomatic Python.
A lambda is a single-expression function with implicit return. The general form is lambda args: expression. No statements, no annotations, no decorators — just an expression and a result.
| 1 | # Syntax: lambda <args>: <expression> |
| 2 | |
| 3 | # Named for reuse (though def is preferred for names) |
| 4 | square = lambda x: x ** 2 |
| 5 | print(square(4)) # → 16 |
| 6 | |
| 7 | # Multiple arguments |
| 8 | add = lambda a, b: a + b |
| 9 | print(add(3, 5)) # → 8 |
| 10 | |
| 11 | # Default argument values are allowed |
| 12 | prefix = lambda text, prefix=">> ": f"{prefix}{text}" |
| 13 | print(prefix("hello")) # → >> hello |
| 14 | print(prefix("bye", "- ")) # → - bye |
| 15 | |
| 16 | # Immediately invoked (IIFE-like, rare in Python) |
| 17 | print((lambda x, y: x if x > y else y)(10, 20)) # → 20 |
| 18 | |
| 19 | # Equivalent def (always clearer for named logic) |
| 20 | def square(x): |
| 21 | return x ** 2 |
The most common use of lambdas in production code is the key argument to sorted(), list.sort(), max(), and min(). A lambda lets you extract a sort key without writing a separate named function.
| 1 | # Sort tuples by second element |
| 2 | pairs = [(1, "z"), (3, "a"), (2, "m")] |
| 3 | pairs.sort(key=lambda p: p[1]) |
| 4 | print(pairs) # → [(3, 'a'), (2, 'm'), (1, 'z')] |
| 5 | |
| 6 | # Sort dicts by a key |
| 7 | users = [ |
| 8 | {"name": "Alice", "age": 30}, |
| 9 | {"name": "Bob", "age": 25}, |
| 10 | {"name": "Carol", "age": 35}, |
| 11 | ] |
| 12 | users.sort(key=lambda u: u["age"]) |
| 13 | print(users[-1]) # → {"name": "Carol", "age": 35} |
| 14 | |
| 15 | # Custom objects — sort by attribute |
| 16 | class Task: |
| 17 | def __init__(self, priority, name): |
| 18 | self.priority = priority |
| 19 | self.name = name |
| 20 | |
| 21 | tasks = [Task(3, "eat"), Task(1, "code"), Task(2, "sleep")] |
| 22 | tasks.sort(key=lambda t: t.priority) |
| 23 | print([t.name for t in tasks]) # → ["code", "sleep", "eat"] |
| 24 | |
| 25 | # Multi-level sort: return a tuple |
| 26 | tasks.sort(key=lambda t: (t.priority, t.name)) |
| 27 | |
| 28 | # max / min with key |
| 29 | longest = max(users, key=lambda u: len(u["name"])) |
| 30 | # → {"name": "Alice", "age": 30} |
info
map() applies a function to every item in an iterable; filter() keeps items where the predicate returns truthy. Both return iterators (lazy), so you wrap them in list() to materialize results.
| 1 | nums = [1, 2, 3, 4, 5] |
| 2 | |
| 3 | # map — transform each element |
| 4 | squares = list(map(lambda x: x ** 2, nums)) |
| 5 | print(squares) # → [1, 4, 9, 16, 25] |
| 6 | |
| 7 | # filter — keep even numbers |
| 8 | evens = list(filter(lambda x: x % 2 == 0, nums)) |
| 9 | print(evens) # → [2, 4] |
| 10 | |
| 11 | # map with multiple iterables (zip-like) |
| 12 | a = [1, 2, 3] |
| 13 | b = [10, 20, 30] |
| 14 | sums = list(map(lambda x, y: x + y, a, b)) |
| 15 | print(sums) # → [11, 22, 33] |
| 16 | |
| 17 | # Chaining — map then filter (hard to read) |
| 18 | result = list( |
| 19 | filter(lambda x: x > 10, |
| 20 | map(lambda x: x * 2, nums)) |
| 21 | ) |
| 22 | print(result) # → [12, 14, 18, 20, 22] (wait: 2,4,6,8,10 → filter >10 → 12,14,18,20,22) |
| 23 | # Actually: map → [2,4,6,8,10], then ... nothing > 10 |
| 24 | # Let's use a better example: |
| 25 | nums2 = [5, 10, 15, 20] |
| 26 | result2 = list( |
| 27 | filter(lambda x: x > 12, |
| 28 | map(lambda x: x + 1, nums2)) |
| 29 | ) |
| 30 | print(result2) # → [16, 21] |
warning
reduce() cumulatively applies a two-argument function to items of an iterable, reducing them to a single value. It lives in functools (removed from built-ins in Python 3).
| 1 | from functools import reduce |
| 2 | import operator |
| 3 | |
| 4 | nums = [1, 2, 3, 4, 5] |
| 5 | |
| 6 | # Sum — equivalent to sum(nums) |
| 7 | total = reduce(lambda a, b: a + b, nums) |
| 8 | print(total) # → 15 |
| 9 | |
| 10 | # Product (no built-in for this) |
| 11 | product = reduce(lambda a, b: a * b, nums) |
| 12 | print(product) # → 120 |
| 13 | |
| 14 | # With initial value |
| 15 | total = reduce(lambda a, b: a + b, nums, 100) |
| 16 | print(total) # → 115 |
| 17 | |
| 18 | # Using operator module (cleaner) |
| 19 | product = reduce(operator.mul, nums, 1) |
| 20 | |
| 21 | # Max without max() |
| 22 | maximum = reduce(lambda a, b: a if a > b else b, nums) |
| 23 | |
| 24 | # Real-world: flatten a list of lists |
| 25 | nested = [[1, 2], [3, 4], [5]] |
| 26 | flat = reduce(lambda acc, xs: acc + xs, nested, []) |
| 27 | print(flat) # → [1, 2, 3, 4, 5] |
| 28 | # But sum(nested, []) or a comprehension is more idiomatic |
| 29 | |
| 30 | # Group-by pattern (like itertools.groupby) |
| 31 | data = [("a", 1), ("b", 2), ("a", 3)] |
| 32 | grouped = reduce(lambda acc, kv: { |
| 33 | **acc, kv[0]: acc.get(kv[0], []) + [kv[1]] |
| 34 | }, data, {}) |
| 35 | print(grouped) # → {"a": [1, 3], "b": [2]} |
The operator module provides function equivalents of every Python operator. Use them to avoid writing trivial lambdas like lambda a, b: a + b.
| 1 | import operator |
| 2 | |
| 3 | # Arithmetic — replaces lambda a,b: a+b etc. |
| 4 | print(operator.add(3, 5)) # → 8 |
| 5 | print(operator.mul(4, 7)) # → 28 |
| 6 | print(operator.truediv(10, 3)) # → 3.333... |
| 7 | |
| 8 | # Comparison — replaces lambda a,b: a>b etc. |
| 9 | sorted(items, key=operator.itemgetter(1)) # sort by second element |
| 10 | |
| 11 | # itemgetter — extract by index or key |
| 12 | from operator import itemgetter |
| 13 | |
| 14 | data = [("Alice", 30), ("Bob", 25), ("Carol", 35)] |
| 15 | get_age = itemgetter(1) |
| 16 | print([get_age(p) for p in data]) # → [30, 25, 35] |
| 17 | |
| 18 | # itemgetter with multiple keys |
| 19 | info = [{"x": 1, "y": 2, "z": 3}, {"x": 4, "y": 5, "z": 6}] |
| 20 | get_xy = itemgetter("x", "y") |
| 21 | print([get_xy(d) for d in info]) # → [(1, 2), (4, 5)] |
| 22 | |
| 23 | # attrgetter — extract attribute by name |
| 24 | from operator import attrgetter |
| 25 | |
| 26 | class Point: |
| 27 | def __init__(self, x, y): |
| 28 | self.x = x |
| 29 | self.y = y |
| 30 | |
| 31 | points = [Point(3, 4), Point(1, 2), Point(5, 1)] |
| 32 | points.sort(key=attrgetter("y")) # sort by y coordinate |
| 33 | print([(p.x, p.y) for p in points]) # → [(5, 1), (1, 2), (3, 4)] |
| 34 | |
| 35 | # methodcaller — call a method by name |
| 36 | from operator import methodcaller |
| 37 | |
| 38 | words = ["hello", "world", "python"] |
| 39 | uppercase = list(map(methodcaller("upper"), words)) |
| 40 | print(uppercase) # → ["HELLO", "WORLD", "PYTHON"] |
| 41 | |
| 42 | # Equivalent lambda: list(map(lambda w: w.upper(), words)) |
| 43 | |
| 44 | # Using operator with reduce |
| 45 | from functools import reduce |
| 46 | nums = [1, 2, 3, 4] |
| 47 | print(reduce(operator.mul, nums)) # → 24 |
| 48 | # vs lambda: reduce(lambda a,b: a*b, nums) |
info
functools.partial freezes a subset of a function's arguments, producing a new callable with fewer parameters. It is the declarative alternative to wrapping a function in a lambda.
| 1 | from functools import partial |
| 2 | |
| 3 | # Simple example: pre-fill an argument |
| 4 | def power(base, exp): |
| 5 | return base ** exp |
| 6 | |
| 7 | square = partial(power, exp=2) |
| 8 | cube = partial(power, exp=3) |
| 9 | |
| 10 | print(square(5)) # → 25 |
| 11 | print(cube(2)) # → 8 |
| 12 | |
| 13 | # Or freeze the first positional arg |
| 14 | def greet(greeting, name): |
| 15 | return f"{greeting}, {name}!" |
| 16 | |
| 17 | say_hello = partial(greet, "Hello") |
| 18 | say_hi = partial(greet, "Hi") |
| 19 | |
| 20 | print(say_hello("Alice")) # → Hello, Alice! |
| 21 | print(say_hi("Bob")) # → Hi, Bob! |
| 22 | |
| 23 | # Real-world: adapt API to callback interface |
| 24 | def write_log(level, message): |
| 25 | print(f"[{level.upper()}] {message}") |
| 26 | |
| 27 | info_logger = partial(write_log, "info") |
| 28 | error_logger = partial(write_log, "error") |
| 29 | |
| 30 | # Pass partial around like any function |
| 31 | def handle_event(logger, event): |
| 32 | logger(f"Event received: {event}") |
| 33 | |
| 34 | handle_event(info_logger, "user_login") |
| 35 | # → [INFO] Event received: user_login |
| 36 | |
| 37 | # partial vs lambda — both work, partial is more intent-revealing |
| 38 | double_lambda = lambda x: x * 2 # opaque |
| 39 | double_partial = partial(operator.mul, 2) # tells you: mul with 2 |
| 40 | |
| 41 | # functools.partialmethod — for binding methods |
| 42 | from functools import partialmethod |
| 43 | |
| 44 | class Widget: |
| 45 | def resize(self, width, height): |
| 46 | self.width = width |
| 47 | self.height = height |
| 48 | |
| 49 | make_square = partialmethod(resize, height=100) |
| 50 | |
| 51 | w = Widget() |
| 52 | w.make_square(width=200) |
| 53 | print(w.width, w.height) # → 200 100 |
note
The Zen of Python says "There should be one — and preferably only one — obvious way to do it." For transforming and filtering iterables, comprehensions are that way. They are more readable, faster in many cases, and avoid lambda overhead entirely.
| 1 | from functools import reduce |
| 2 | |
| 3 | nums = range(20) |
| 4 | |
| 5 | # --- map --- |
| 6 | # Lambda style: |
| 7 | squares = list(map(lambda x: x ** 2, nums)) |
| 8 | # Comprehension (preferred): |
| 9 | squares = [x ** 2 for x in nums] |
| 10 | |
| 11 | # --- filter --- |
| 12 | # Lambda style: |
| 13 | evens = list(filter(lambda x: x % 2 == 0, nums)) |
| 14 | # Comprehension (preferred): |
| 15 | evens = [x for x in nums if x % 2 == 0] |
| 16 | |
| 17 | # --- map + filter (chained) --- |
| 18 | # Lambda style (backward — reads inside-out): |
| 19 | result = list( |
| 20 | filter(lambda x: x > 50, |
| 21 | map(lambda x: x ** 2, |
| 22 | filter(lambda x: x % 2 == 0, nums))) |
| 23 | ) |
| 24 | # Comprehension (reads left-to-right in one pass): |
| 25 | result = [x ** 2 for x in nums if x % 2 == 0 and x ** 2 > 50] |
| 26 | |
| 27 | # --- reduce --- |
| 28 | # No comprehension equivalent — reduce has its niche. |
| 29 | total = reduce(lambda a, b: a + b, nums) |
| 30 | |
| 31 | # --- When lambdas + map/filter are OK --- |
| 32 | # 1. You already have a named function (no lambda needed): |
| 33 | list(map(str.upper, ["a", "b", "c"])) |
| 34 | # Better than: [s.upper() for s in ["a", "b", "c"]] (debatable) |
| 35 | |
| 36 | # 2. Working with methodcaller on foreign objects: |
| 37 | from operator import methodcaller |
| 38 | list(map(methodcaller("strip"), strings)) |
| 39 | |
| 40 | # 3. Converting types: |
| 41 | list(map(int, ["1", "2", "3"])) # reads nicely |
| 42 | # vs [int(s) for s in ["1", "2", "3"]] |
info
Lambdas are deliberately crippled — they can only contain a single expression. No statements, no annotations, no decorators. If you need any of those, write a def.
| 1 | # Limitation 1: Single expression only |
| 2 | # These will NOT work: |
| 3 | # lambda x: return x + 1 # SyntaxError |
| 4 | # lambda x: if x > 0: x # SyntaxError |
| 5 | # lambda x: y = x + 1; return y # SyntaxError |
| 6 | |
| 7 | # Any statement is off-limits: if, for, while, try, with, match... |
| 8 | |
| 9 | # Workaround: use conditional expression (ternary) |
| 10 | f = lambda x: "positive" if x > 0 else "non-positive" |
| 11 | |
| 12 | # Limitation 2: No type annotations |
| 13 | # lambda x: int: x + 1 # SyntaxError |
| 14 | |
| 15 | # Limitation 3: No decorators |
| 16 | |
| 17 | # Limitation 4: Hard to debug — tracebacks show "<lambda>" |
| 18 | add = lambda a, b: a + b |
| 19 | # If this raises, the traceback says "<lambda>" not "add" |
| 20 | |
| 21 | # Limitation 5: Closure gotchas with late binding |
| 22 | # Common mistake in list comprehensions: |
| 23 | funcs = [lambda: i for i in range(5)] |
| 24 | print([f() for f in funcs]) # → [4, 4, 4, 4, 4] (all capture the same i!) |
| 25 | |
| 26 | # Fix: capture by default argument |
| 27 | funcs = [lambda i=i: i for i in range(5)] |
| 28 | print([f() for f in funcs]) # → [0, 1, 2, 3, 4] |
| 29 | |
| 30 | # Or use a def inside the loop: |
| 31 | funcs = [] |
| 32 | for i in range(5): |
| 33 | def make_f(i=i): # default arg binds the current value |
| 34 | return i |
| 35 | funcs.append(make_f) |
| 36 | |
| 37 | # Limitation 6: No annotations means no type-checking help |
| 38 | # def is better: |
| 39 | def add(a: int, b: int) -> int: |
| 40 | return a + b |
| 41 | |
| 42 | # When to just use def: |
| 43 | # - More than one expression |
| 44 | # - Needs a docstring |
| 45 | # - Has side effects beyond the return value |
| 46 | # - Complex conditional logic |
| 47 | # - You want type hints |
note
| 1 | # Pattern 1: Sorting by computed key |
| 2 | # Sort files by extension, then by name |
| 3 | files = ["data.csv", "main.py", "notes.txt", "utils.py"] |
| 4 | files.sort(key=lambda f: (f.split(".")[-1], f)) |
| 5 | # → ["data.csv", "main.py", "utils.py", "notes.txt"] |
| 6 | |
| 7 | # Pattern 2: Default factory for defaultdict |
| 8 | from collections import defaultdict |
| 9 | |
| 10 | nested = defaultdict(lambda: defaultdict(list)) |
| 11 | nested["users"]["posts"].append("Hello") |
| 12 | # No lambda alternative using def here is cleaner |
| 13 | |
| 14 | # Pattern 3: Tkinter / GUI callbacks |
| 15 | # button = Button(text="Click", command=lambda: print("clicked")) |
| 16 | |
| 17 | # Pattern 4: Min/max with complex selection |
| 18 | tasks = [ |
| 19 | {"name": "review", "priority": 2, "est": 5}, |
| 20 | {"name": "deploy", "priority": 1, "est": 10}, |
| 21 | {"name": "test", "priority": 3, "est": 2}, |
| 22 | ] |
| 23 | # Most urgent: lowest priority, then longest estimate |
| 24 | most_urgent = min(tasks, key=lambda t: (t["priority"], -t["est"])) |
| 25 | print(most_urgent["name"]) # → deploy |
| 26 | |
| 27 | # Pattern 5: Partial + operator for DRY code |
| 28 | from functools import partial |
| 29 | import operator |
| 30 | |
| 31 | # Database query builder mock |
| 32 | def query(table, condition, value): |
| 33 | return f"SELECT * FROM {table} WHERE {condition} = {value}" |
| 34 | |
| 35 | eq = partial(query, condition="id") |
| 36 | gt = partial(query, condition="age >") |
| 37 | |
| 38 | print(eq(table="users", value=42)) |
| 39 | # → SELECT * FROM users WHERE id = 42 |
| 40 | |
| 41 | print(gt(table="users", value=18)) |
| 42 | # → SELECT * FROM users WHERE age > 18 |
| 43 | |
| 44 | # Pattern 6: Cached property with lambda default |
| 45 | from functools import cached_property |
| 46 | |
| 47 | class Circle: |
| 48 | def __init__(self, radius): |
| 49 | self.radius = radius |
| 50 | |
| 51 | # lambda is overkill here; just use a method |
| 52 | @cached_property |
| 53 | def area(self): |
| 54 | return 3.14159 * self.radius ** 2 |