# Functions in Depth: Default Values, Functions as Objects, *args, **kwargs and Lambda<no value>

## 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**:

```python
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:

```python
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.

{{< box "analogy" "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.
{{< /box >}}

### `*args` — a variable number of arguments

An asterisk before the parameter's name says: "collect **all** the regular arguments (by position) into a **tuple**":

```python
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**":

```python
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** = **k**ey**w**ord **arg**ument**s**.

### Lambda

**lambda** is a way to write a **small function, in one line, without a name**:

```python
lambda x: x * 2
```

Equivalent to:

```python
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

```python
# 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`:

```python
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:**

```text
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`:

```python
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:**

```text
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.**

```python
print(say("Hello", shout()))
```

```text
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.**

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
```

```text
['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:

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
```

**Mistake 3: a lambda that is too complicated.**

```python
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.**

```python
def greet(greeting="Hello", name):
```

```text
SyntaxError: parameter without a default follows parameter with a default
```

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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="!")`.

```python
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
```

**Expected output:**

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
```

{{< solution >}}
```python
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="?"))
```

```text
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.
{{< /solution >}}

### 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.

```python
print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
```

**Expected output:**

```text
85.0
85.0
error: need at least one number
```

{{< solution >}}
```python
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)
```

```text
85.0
85.0
error: need at least one number
```

`*numbers` collects all the arguments by position into a **tuple**.
{{< /solution >}}

### 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`.

```python
print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
```

**Expected output:**

```text
Dana: age=24 city=Haifa
Avi:
```

{{< solution >}}
```python
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"))
```

```text
Dana: age=24 city=Haifa
Avi:
```

`**details` collects all the **named** arguments into a **dict**.
{{< /solution >}}

### Exercise 4: lambda in sorting

```python
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:**

```text
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
```

{{< solution >}}
```python
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])
```

```text
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.
{{< /solution >}}

### Exercise 5: A mutable default

```python
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.

{{< 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:

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart


print(add_item("milk"))
print(add_item("bread"))
```

```text
['milk']
['bread']
```
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
- [Arbitrary Argument Lists](https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists)
- [Keyword Arguments](https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments)
- [Lambda Expressions](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions)

<!-- docs:end -->
