Diane's latest request: the warehouse product list needs to be sortable six different ways depending on which report she's running — by price, by stock, by category, by SKU. How would you build that with what you have right now?
I'd write a helper for each sort. sort_by_price, sort_by_stock, sort_by_category. They'd be almost identical — each one calls sorted() with a different key pulled out of each product dict.
How many of those helpers are you going to write before you notice the repetition?
Probably four before I get annoyed with myself. The structure is always the same — sorted(), pull a specific field, return. I'd be copy-pasting and changing one word each time.
And when Diane asks for a fifth sort next week?
I write a fifth function and pretend the scope didn't creep.
Or you write one function that accepts the sort key as a parameter, and you use a lambda to extract that key from each product. That's the problem lambda was built to solve. One function, infinite sort modes.
I've seen lambda before. Every time I look at it I think "why not just write a real function?" It feels like a shortcut that makes the code harder to read.
That is the correct instinct and I don't want you to lose it. Lambda is not a replacement for def. Think about how a label printer works at the packing dock. For most shipments you print the label right at packing — you don't name the printer, you don't save the template, you print one label and move on. Lambda is the same thing: a throwaway function that does one job and never needs a name.
So the question is whether the function deserves a name. If I'd never call it from anywhere else and it's simple enough to read inline, it doesn't need one?
Exactly. The rule: if you'd have to think of a name for it, you'd only call it in one place, and it fits on one line — that's a lambda candidate. Here's the syntax:
lambda argument: expressionOne parameter, one expression, implicit return. No def, no name, no return keyword. The expression after the colon is always the return value:
# Assigned to a variable just to show the shape — don't do this in production
get_price = lambda product: product["price"]
print(get_price({"name": "Widget-A", "price": 24.99})) # 24.99No return statement — the expression just evaluates and that's the output. And the moment you need more than one expression, you can't use lambda at all?
Correct. One expression only. That constraint is a feature — it forces you toward def the moment the logic gets complex. Now watch where lambda actually earns its place:
products = [
{"name": "Widget-A", "price": 24.99, "stock": 150, "category": "Hardware"},
{"name": "Safety-Vest","price": 14.99, "stock": 60, "category": "Safety"},
{"name": "Drill-Bit", "price": 9.99, "stock": 200, "category": "Hardware"},
]
# Sort by price — ascending
by_price = sorted(products, key=lambda p: p["price"])
print(by_price[0]["name"]) # Drill-Bit
# Sort by stock — descending
by_stock = sorted(products, key=lambda p: p["stock"], reverse=True)
print(by_stock[0]["name"]) # Drill-Bitsorted() calls the lambda once per item? I'm handing it a recipe and it applies the recipe to decide where each item goes?
That's it. key= accepts any callable — function, method, lambda. It calls that callable on each element and sorts by the results. The lambda is just a compact way to say "for each product, use this field to determine its position." Now let's solve Diane's problem:
def sort_inventory(products: list[dict], sort_key: str, reverse: bool = False) -> list[dict]:
return sorted(products, key=lambda p: p[sort_key], reverse=reverse)One line. sort_key is a string I receive as a parameter, and the lambda captures it from the outer scope. I can call sort_inventory(products, "price") or sort_inventory(products, "stock", reverse=True) and the same function handles both.
That's closure — the lambda sees variables from the scope where it was created. sort_key lives in sort_inventory, and the lambda defined inside it can read that value. This is one of the most common lambda patterns in real Python code.
I still want to push back on readability. key=lambda p: p["price"] is fine. But key=lambda p: p["category"].lower().strip() starts looking like a puzzle. Where's the line?
That's the line. The moment your lambda needs more than a simple field lookup or basic arithmetic, give it a name with def. The readable case is when the lambda reads almost like English: "key equals each product's price." The unreadable case is when someone has to stop and decode it. Trust that instinct.
Fine. I hate the syntax but I understand why it exists. Those are different things.
"I hate it but I get it" — a perfect summary of how most Python developers feel about lambda. One more rule: you can assign a lambda to a variable, but the style guide says don't. If you need a named function, write def. Linters flag assigned lambdas because a named function is always more readable than a named lambda.
Lambda is anonymous by design. If you're naming it, you should be using def instead. Got it.
Tomorrow: docstrings — writing the contents on the outside of the box. And we tackle the * on the call side, not the definition side: what happens when your arguments are already in a list or dict and you want to unpack them directly into function parameters.
Same symbol, different side of the call?
Same symbol, opposite direction. One side collects, the other spreads. Find out tomorrow.
A lambda expression creates an anonymous, single-expression function inline. The full syntax is lambda parameters: expression. The expression is always returned implicitly — there is no return keyword.
# Lambda as a one-time key function
products = [
{"name": "Widget-A", "price": 24.99, "stock": 150},
{"name": "Drill-Bit", "price": 9.99, "stock": 200},
{"name": "Bolt-M6", "price": 4.99, "stock": 0},
]
# Sort by stock descending
reorder = sorted(products, key=lambda p: p["stock"])
print(reorder[0]["name"]) # Bolt-M6 — lowest stock first
# Sort by price descending
pricey = sorted(products, key=lambda p: p["price"], reverse=True)
print(pricey[0]["name"]) # Widget-A — most expensive firstWhen lambda is appropriate:
sorted, map, filter, or min/max)get_price is a name, but lambda p: p["price"] already reads like oneClosure: A lambda can read variables from the enclosing scope. When sort_key is a parameter of the outer function, the lambda defined inside that function captures it automatically:
def sort_inventory(products: list[dict], sort_key: str, reverse: bool = False) -> list[dict]:
return sorted(products, key=lambda p: p[sort_key], reverse=reverse)The lambda knows sort_key because it was defined in the same scope. This is the same closure mechanism that applies to nested def functions.
Pitfall 1: Assigning lambda to a variable. get_price = lambda p: p["price"] works but violates PEP 8. The style guide explicitly states: if the lambda needs a name, use def. Linters (flake8, ruff) flag assigned lambdas as E731.
Pitfall 2: Multi-line or multi-statement lambdas. Lambda accepts exactly one expression. Semicolons, assignments, and return statements are all syntax errors inside a lambda body. If you need more logic, write a def.
Pitfall 3: Late-binding closures in loops. If you create lambdas in a loop and each lambda is supposed to capture the current loop variable, they all capture the final value instead:
funcs = [lambda: i for i in range(3)]
print(funcs[0]()) # 2 — not 0! All three captured the same `i`Fix: use a default argument to bind the current value: lambda i=i: i.
operator.itemgetter("price") is a faster alternative to lambda p: p["price"] for large datasets — it avoids Python function call overhead. operator.attrgetter("stock") does the same for attribute access on objects. Both are worth knowing for performance-critical sort paths.
filter() and map() are the other canonical lambda hosts — filter(lambda p: p["stock"] > 0, products) returns only in-stock items. In modern Python, list comprehensions are generally preferred over filter/map for readability, but lambdas remain common in sorted() and min()/max() key arguments throughout the standard library.
Sign up to write and run code in this lesson.
Diane's latest request: the warehouse product list needs to be sortable six different ways depending on which report she's running — by price, by stock, by category, by SKU. How would you build that with what you have right now?
I'd write a helper for each sort. sort_by_price, sort_by_stock, sort_by_category. They'd be almost identical — each one calls sorted() with a different key pulled out of each product dict.
How many of those helpers are you going to write before you notice the repetition?
Probably four before I get annoyed with myself. The structure is always the same — sorted(), pull a specific field, return. I'd be copy-pasting and changing one word each time.
And when Diane asks for a fifth sort next week?
I write a fifth function and pretend the scope didn't creep.
Or you write one function that accepts the sort key as a parameter, and you use a lambda to extract that key from each product. That's the problem lambda was built to solve. One function, infinite sort modes.
I've seen lambda before. Every time I look at it I think "why not just write a real function?" It feels like a shortcut that makes the code harder to read.
That is the correct instinct and I don't want you to lose it. Lambda is not a replacement for def. Think about how a label printer works at the packing dock. For most shipments you print the label right at packing — you don't name the printer, you don't save the template, you print one label and move on. Lambda is the same thing: a throwaway function that does one job and never needs a name.
So the question is whether the function deserves a name. If I'd never call it from anywhere else and it's simple enough to read inline, it doesn't need one?
Exactly. The rule: if you'd have to think of a name for it, you'd only call it in one place, and it fits on one line — that's a lambda candidate. Here's the syntax:
lambda argument: expressionOne parameter, one expression, implicit return. No def, no name, no return keyword. The expression after the colon is always the return value:
# Assigned to a variable just to show the shape — don't do this in production
get_price = lambda product: product["price"]
print(get_price({"name": "Widget-A", "price": 24.99})) # 24.99No return statement — the expression just evaluates and that's the output. And the moment you need more than one expression, you can't use lambda at all?
Correct. One expression only. That constraint is a feature — it forces you toward def the moment the logic gets complex. Now watch where lambda actually earns its place:
products = [
{"name": "Widget-A", "price": 24.99, "stock": 150, "category": "Hardware"},
{"name": "Safety-Vest","price": 14.99, "stock": 60, "category": "Safety"},
{"name": "Drill-Bit", "price": 9.99, "stock": 200, "category": "Hardware"},
]
# Sort by price — ascending
by_price = sorted(products, key=lambda p: p["price"])
print(by_price[0]["name"]) # Drill-Bit
# Sort by stock — descending
by_stock = sorted(products, key=lambda p: p["stock"], reverse=True)
print(by_stock[0]["name"]) # Drill-Bitsorted() calls the lambda once per item? I'm handing it a recipe and it applies the recipe to decide where each item goes?
That's it. key= accepts any callable — function, method, lambda. It calls that callable on each element and sorts by the results. The lambda is just a compact way to say "for each product, use this field to determine its position." Now let's solve Diane's problem:
def sort_inventory(products: list[dict], sort_key: str, reverse: bool = False) -> list[dict]:
return sorted(products, key=lambda p: p[sort_key], reverse=reverse)One line. sort_key is a string I receive as a parameter, and the lambda captures it from the outer scope. I can call sort_inventory(products, "price") or sort_inventory(products, "stock", reverse=True) and the same function handles both.
That's closure — the lambda sees variables from the scope where it was created. sort_key lives in sort_inventory, and the lambda defined inside it can read that value. This is one of the most common lambda patterns in real Python code.
I still want to push back on readability. key=lambda p: p["price"] is fine. But key=lambda p: p["category"].lower().strip() starts looking like a puzzle. Where's the line?
That's the line. The moment your lambda needs more than a simple field lookup or basic arithmetic, give it a name with def. The readable case is when the lambda reads almost like English: "key equals each product's price." The unreadable case is when someone has to stop and decode it. Trust that instinct.
Fine. I hate the syntax but I understand why it exists. Those are different things.
"I hate it but I get it" — a perfect summary of how most Python developers feel about lambda. One more rule: you can assign a lambda to a variable, but the style guide says don't. If you need a named function, write def. Linters flag assigned lambdas because a named function is always more readable than a named lambda.
Lambda is anonymous by design. If you're naming it, you should be using def instead. Got it.
Tomorrow: docstrings — writing the contents on the outside of the box. And we tackle the * on the call side, not the definition side: what happens when your arguments are already in a list or dict and you want to unpack them directly into function parameters.
Same symbol, different side of the call?
Same symbol, opposite direction. One side collects, the other spreads. Find out tomorrow.
A lambda expression creates an anonymous, single-expression function inline. The full syntax is lambda parameters: expression. The expression is always returned implicitly — there is no return keyword.
# Lambda as a one-time key function
products = [
{"name": "Widget-A", "price": 24.99, "stock": 150},
{"name": "Drill-Bit", "price": 9.99, "stock": 200},
{"name": "Bolt-M6", "price": 4.99, "stock": 0},
]
# Sort by stock descending
reorder = sorted(products, key=lambda p: p["stock"])
print(reorder[0]["name"]) # Bolt-M6 — lowest stock first
# Sort by price descending
pricey = sorted(products, key=lambda p: p["price"], reverse=True)
print(pricey[0]["name"]) # Widget-A — most expensive firstWhen lambda is appropriate:
sorted, map, filter, or min/max)get_price is a name, but lambda p: p["price"] already reads like oneClosure: A lambda can read variables from the enclosing scope. When sort_key is a parameter of the outer function, the lambda defined inside that function captures it automatically:
def sort_inventory(products: list[dict], sort_key: str, reverse: bool = False) -> list[dict]:
return sorted(products, key=lambda p: p[sort_key], reverse=reverse)The lambda knows sort_key because it was defined in the same scope. This is the same closure mechanism that applies to nested def functions.
Pitfall 1: Assigning lambda to a variable. get_price = lambda p: p["price"] works but violates PEP 8. The style guide explicitly states: if the lambda needs a name, use def. Linters (flake8, ruff) flag assigned lambdas as E731.
Pitfall 2: Multi-line or multi-statement lambdas. Lambda accepts exactly one expression. Semicolons, assignments, and return statements are all syntax errors inside a lambda body. If you need more logic, write a def.
Pitfall 3: Late-binding closures in loops. If you create lambdas in a loop and each lambda is supposed to capture the current loop variable, they all capture the final value instead:
funcs = [lambda: i for i in range(3)]
print(funcs[0]()) # 2 — not 0! All three captured the same `i`Fix: use a default argument to bind the current value: lambda i=i: i.
operator.itemgetter("price") is a faster alternative to lambda p: p["price"] for large datasets — it avoids Python function call overhead. operator.attrgetter("stock") does the same for attribute access on objects. Both are worth knowing for performance-critical sort paths.
filter() and map() are the other canonical lambda hosts — filter(lambda p: p["stock"] > 0, products) returns only in-stock items. In modern Python, list comprehensions are generally preferred over filter/map for readability, but lambdas remain common in sorted() and min()/max() key arguments throughout the standard library.