Functions in Depth: Default Values, Functions as Objects, *args, **kwargs and Lambda

Lesson goals
  • 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 *args and **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:

  1. Default values and named arguments.
  2. Functions as Objects — a function is a value like any other value.
  3. *args — as many regular arguments as you want.
  4. **kwargs — as many named arguments as you want.
  5. 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 + ", " + name

greet("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.

Analogy: a recipe versus a cake

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 * 2

Equivalent to:

def double(x):
    return x * 2
  • After lambda: the parameters. After :: one expression that is the returned value (without return).
  • The main use: passing a short function as an argument, instead of defining a separate def. Mostly with sorted, 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

LineExplanation
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: Mouse

Breakdown:

  • def order_total(*prices, discount=0): — *prices collects any number of prices into a tuple. discount comes 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, ...) — username gets "avi", and all the rest is collected into extra as a dict: {"age": 30, "city": "Haifa", "is_admin": True}.
  • sorted(products, key=lambda product: product["price"]) — sorted does not know how to compare dicts. The key is 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 cart

Mistake 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 default

Practice

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 number
Show 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)]
  1. Sort by price, from the cheapest to the most expensive, and print every product.
  2. Print the name of the most expensive product.

Expected output:

Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
Show 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: Laptop

lambda 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"))
  1. What will be printed in the second call, and why?
  2. Fix the function.
Show solution
  1. ['milk', 'bread'] will be printed. The default [] is created once, when the function is defined — and all the calls share the same list.
  2. 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 (use None).
  • 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.
  • *args collects regular arguments into a tuple. **kwargs collects named arguments into a dict.
  • lambda x: expression is a short function without a name. Perfect for the key= of sorted/min/max. If it is complicated, a def is better.

Official documentation