Functions in Depth: Default Values, Functions as Objects, *args, **kwargs and Lambda
- Define parameters with a default value and call a function with named arguments
- Understand that a function is an object — it can be stored in a variable and passed to another function
- Write functions that receive a variable number of arguments with
*argsand**kwargs - Write a simple lambda and use it with
sorted(key=...)
What will we learn?
In the clean code and debugging topic we learned the basics: def, parameters, return. Today we will learn a few abilities that make functions much more flexible:
- Default values and named arguments.
- Functions as Objects — a function is a value like any other value.
*args— as many regular arguments as you want.**kwargs— as many named arguments as you want.- Lambda — a short function without a name.
Why do we need this?
print("a", "b", "c", sep="-")receives as many arguments as you want, plus a named argument. How is it built? With*args.sorted(students, key=...)receives a function as a parameter, to know what to sort by.- FastAPI (the backend and databases topics) is built entirely on the idea that a function is an object: you write a function, and FastAPI “registers” it as an Endpoint and runs it when a request arrives.
Explanation from the basics
Default Values
You can give a parameter a value that will be used if no argument was passed:
def greet(name, greeting="Hello"):
return greeting + ", " + namegreet("Dana") → "Hello, Dana". greet("Dana", "Hi") → "Hi, Dana".
Rule: parameters with a default come after parameters without one.
Keyword Arguments
In the call you can state the name of a parameter, and then the order no longer matters:
greet(greeting="Hey", name="Avi")This is very readable when there are many parameters: create_user("dana", age=24, is_admin=False).
Functions as Objects
In Python, a function is an object, like a number or a list. So you can:
- Store it in a variable:
say = greet. - Put it in a list or a dict.
- Pass it as an argument to another function.
The critical difference: greet (without brackets) = the function itself. greet("Dana") (with brackets) = running it, and its result.
greet is the recipe: you can photograph it and send it to a friend. greet("Dana") is the cake: the result of baking according to the recipe. When you pass a function to another function, you send the recipe, so that it can bake from it whenever it needs to.
*args — a variable number of arguments
An asterisk before the parameter’s name says: “collect all the regular arguments (by position) into a tuple”:
def total(*numbers):
return sum(numbers)
total(1, 2) # numbers = (1, 2)
total(5, 10, 20, 1) # numbers = (5, 10, 20, 1)The name args is a convention. The asterisk is what does the work.
**kwargs — a variable number of named arguments
Two asterisks: “collect all the named arguments into a dict”:
def describe(**details):
for key, value in details.items():
print(key, "=", value)
describe(name="Dana", age=24, city="Haifa")
# details = {"name": "Dana", "age": 24, "city": "Haifa"}kwargs = keyword arguments.
Lambda
lambda is a way to write a small function, in one line, without a name:
lambda x: x * 2Equivalent to:
def double(x):
return x * 2- After
lambda: the parameters. After:: one expression that is the returned value (withoutreturn). - The main use: passing a short function as an argument, instead of defining a separate
def. Mostly withsorted,min,max.
Syntax
# default value + keyword arguments
def func(required, optional="default"):
...
func("a")
func("a", optional="b")
# function as an object
f = func # no parentheses: the function itself
f("a") # call it through the new name
def apply(operation, value):
return operation(value) # call the function we received
# *args and **kwargs
def func(*args, **kwargs):
# args -> tuple of positional arguments
# kwargs -> dict of keyword arguments
...
# lambda
lambda parameters: expression
sorted(items, key=lambda item: item["price"])First code example
week-2/functions_deep.py:
def make_greeting(name, greeting="Hello", punctuation="!"):
return greeting + ", " + name + punctuation
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punctuation="?"))
def shout(text):
return text.upper()
def whisper(text):
return text.lower() + "..."
def say(message, style):
return style(message)
print(say("Hello there", shout))
print(say("Hello there", whisper))Output:
Hello, Dana!
Good morning, Avi!
Hello, Noa?
HELLO THERE
hello there...Breaking down the code line by line
| Line | Explanation |
|---|---|
def make_greeting(name, greeting="Hello", punctuation="!") | name is required. The other two are optional, with defaults. |
make_greeting("Dana") | Both optional parameters take their default. |
make_greeting("Avi", "Good morning") | A second argument by position goes into greeting. |
make_greeting("Noa", punctuation="?") | A named argument: we skip greeting (default) and set only punctuation. |
def say(message, style): | style is a parameter that expects a function. |
return style(message) | Running the function we received. say does not know and does not care which function it is. |
say("Hello there", shout) | We pass shout without brackets, that is, the function itself. |
Another example
*args, **kwargs and lambda in a small real system. week-2/store_tools.py:
def order_total(*prices, discount=0):
subtotal = sum(prices)
return subtotal - subtotal * discount / 100
print(order_total(10, 20, 30))
print(order_total(100, 50, discount=10))
def build_user(username, **extra):
user = {"username": username}
for key, value in extra.items():
user[key] = value
return user
print(build_user("dana"))
print(build_user("avi", age=30, city="Haifa", is_admin=True))
products = [
{"name": "Laptop", "price": 3500},
{"name": "Mouse", "price": 80},
{"name": "Desk", "price": 900},
]
by_price = sorted(products, key=lambda product: product["price"])
for product in by_price:
print(product["name"], product["price"])
cheapest = min(products, key=lambda product: product["price"])
print("Cheapest:", cheapest["name"])Output:
60.0
135.0
{'username': 'dana'}
{'username': 'avi', 'age': 30, 'city': 'Haifa', 'is_admin': True}
Mouse 80
Desk 900
Laptop 3500
Cheapest: MouseBreakdown:
def order_total(*prices, discount=0):—*pricescollects any number of prices into a tuple.discountcomes after*prices, so it can be passed only by name (discount=10). Otherwise, Python would not know where the prices end.build_user("avi", age=30, ...)—usernamegets"avi", and all the rest is collected intoextraas a dict:{"age": 30, "city": "Haifa", "is_admin": True}.sorted(products, key=lambda product: product["price"])—sorteddoes not know how to compare dicts. Thekeyis a function that tells it: “for every product, use its price to compare”. The lambda is called once for every product.min(..., key=...)— the same idea: “the minimum by price”. It returns the whole product, not only the price.
Common mistake
Mistake 1: passing a result instead of a function.
print(say("Hello", shout()))TypeError: shout() missing 1 required positional argument: 'text'shout() with brackets tries to run shout immediately, without an argument. When you pass a function, write only the name: shout.
Mistake 2: a list or a dict as a default value.
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("milk"))
print(add_item("bread"))['milk']
['milk', 'bread']We expected ['bread'] in the second call! The default value [] is created once, when the function is defined, and all the calls share the same list (Mutable, the previous lesson!). The accepted fix:
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cartMistake 3: a lambda that is too complicated.
result = sorted(users, key=lambda u: (0 if u["active"] else 1, -u["score"], u["name"].lower()))It works, but nobody will understand it in a week (KISS!). If a lambda is not readable at a glance, write a def with a clear name.
Mistake 4: a parameter with a default before a required parameter.
def greet(greeting="Hello", name):SyntaxError: parameter without a default follows parameter with a defaultPractice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
Exercise 1: Defaults and named arguments
Write a function make_greeting(name, greeting="Hello", punct="!").
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))Expected output:
Hello, Dana!
Good morning, Avi!
Hello, Noa?Show solution
def make_greeting(name, greeting="Hello", punct="!"):
return greeting + ", " + name + punct
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))Hello, Dana!
Good morning, Avi!
Hello, Noa?In the third call punct="?" is passed by name, so you can skip greeting and leave it its default.
Exercise 2: *args
Write a function average(*numbers) that receives any number of numbers and returns the average. If no number was passed — it throws a ValueError with the message shown in the expected output.
print(average(80, 90))
print(average(70, 80, 90, 100))
try:
average()
except ValueError as error:
print("error:", error)Expected output:
85.0
85.0
error: need at least one numberShow solution
def average(*numbers):
if len(numbers) == 0:
raise ValueError("need at least one number")
return sum(numbers) / len(numbers)
print(average(80, 90))
print(average(70, 80, 90, 100))
try:
average()
except ValueError as error:
print("error:", error)85.0
85.0
error: need at least one number*numbers collects all the arguments by position into a tuple.
Exercise 3: **kwargs
Write a function format_profile(name, **details) that returns a line with the name, followed by every extra detail in the format key=value.
print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))Expected output:
Dana: age=24 city=Haifa
Avi:Show solution
def format_profile(name, **details):
line = name + ":"
for key, value in details.items():
line = line + " " + key + "=" + str(value)
return line
print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))Dana: age=24 city=Haifa
Avi:**details collects all the named arguments into a dict.
Exercise 4: lambda in sorting
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]- Sort by price, from the cheapest to the most expensive, and print every product.
- Print the name of the most expensive product.
Expected output:
Mouse 80
Desk 900
Laptop 3500
Most expensive: LaptopShow solution
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]
for name, price in sorted(products, key=lambda p: p[1]):
print(name, price)
most_expensive = max(products, key=lambda p: p[1])
print("Most expensive:", most_expensive[0])Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptoplambda p: p[1] is a short function that returns the price — sorted and max compare by it.
Exercise 5: A mutable default
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("milk"))
print(add_item("bread"))- What will be printed in the second call, and why?
- Fix the function.
Show solution
['milk', 'bread']will be printed. The default[]is created once, when the function is defined — and all the calls share the same list.- Use
None, and create a new list inside the function:
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(add_item("milk"))
print(add_item("bread"))['milk']
['bread']Summary
- A default:
def f(a, b=10). Parameters with a default come last. Never[]or{}as a default (useNone). - A named argument:
f(a=1, b=2). Readable, and the order does not matter. - A function is an object:
f= the function,f()= running it. You can store it in a variable, in a dict, and pass it as an argument. *argscollects regular arguments into a tuple.**kwargscollects named arguments into a dict.lambda x: expressionis a short function without a name. Perfect for thekey=ofsorted/min/max. If it is complicated, adefis better.