# Higher-Order Functions: Functions That Receive and Return Functions<no value>

## What will we learn?

- What a **Higher-Order Function** is.
- How to **pass a function as a parameter** — and why without brackets.
- The built-in functions **`map`** and **`filter`**.
- The **`key`** parameter of `sorted`, `max` and `min`.
- **A function that returns a function**.

## Why do we need this?

Two almost identical functions:

```python
def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result

def square_all(numbers):
    result = []
    for n in numbers:
        result.append(n * n)
    return result
```

The loop is **identical** — only the action on each item is different. Every new action (half, plus 10, rounding...) requires another whole function, and that is exactly the duplication DRY forbids.

**The idea:** write the loop **once**, and pass **the action itself** as a parameter.

## Explanation from the basics

### A function is an object

We already saw in the lesson "functions in depth" that in Python a function is an **object** — like a number or a list:

- `double` (without brackets) — **the function itself**. You can store it in a variable or pass it on.
- `double(5)` (with brackets) — **a call** to the function. The result is what it returns.

### Higher-Order Function

A **Higher-Order Function** is a function that:

- **Receives** another function as a parameter, **or**
- **Returns** a function.

You have already used one: `sorted(products, key=lambda p: p[1])` — `sorted` receives a function in the `key` parameter.

{{< box "analogy" "Analogy: a washing machine with programs" >}}
A washing machine knows how to "go over all the clothes". **What** to do to each piece — that you choose with the program: delicate wash, regular or drying. You don't buy a new machine for every program — you give it the program. The Higher-Order Function is the machine, and the function you pass is the program.
{{< /box >}}

## Syntax

```python
def apply_to_all(numbers, action):     # action is a function
    result = []
    for n in numbers:
        result.append(action(n))       # call it for each item
    return result

apply_to_all([1, 2, 3], double)        # pass it: no parentheses
list(map(double, [1, 2, 3]))           # built-in: map
list(filter(is_even, [1, 2, 3, 4]))    # built-in: filter
sorted(words, key=len)                 # key: sort by the result of len
```

## First code example

`week-2/apply_to_all.py`:

```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result

def double(n):
    return n * 2

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

**Output:**

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `def apply_to_all(numbers, action):` | `action` is a regular parameter — only we expect it to hold a **function**. |
| `result.append(action(n))` | We call the function we received, on every item. |
| `apply_to_all([1, 2, 3], double)` | We pass `double` **without brackets** — the function, not its result. |
| `lambda n: n * n` | A short function without a name, created on the spot. |
| `round` | A built-in function can be passed too. |

## Another example

**`map`, `filter` and `key`.** `week-2/hof_builtins.py`:

```python
prices = [100, 250, 40, 80]

with_fee = list(map(lambda p: p + 10, prices))
cheap = list(filter(lambda p: p < 100, prices))
print(with_fee)
print(cheap)

words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len))
print(max(words, key=len))

students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
print(max(students, key=lambda s: s[1]))
```

**Output:**

```text
[110, 260, 50, 90]
[40, 80]
['fig', 'kiwi', 'banana', 'cherry']
banana
('Dana', 92)
```

- **`map(func, items)`** — runs `func` on **every** item. `list(...)` turns the result into a list.
- **`filter(func, items)`** — keeps only the items for which `func` returns `True`.
- **`key=len`** — `sorted` and `max` compare by **the result** of `len` on each word, not by the word itself.

**A function that returns a function.** `week-2/make_multiplier.py`:

```python
def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))
```

**Output:**

```text
10
15
```

`make_multiplier` is a "factory" that produces functions: every call returns a **new** function, which remembers its own `factor`. How exactly it remembers — we will see in the next lesson, on nested functions.

## Common mistake

**The mistake: calling the function instead of passing it.**

```python
apply_to_all([1, 2], double())
```

```text
TypeError: double() missing 1 required positional argument: 'n'
```

**Why?** `double()` with brackets **calls** the function immediately — without a parameter — so it fails even before `apply_to_all` starts. Pass the function **without brackets**: `apply_to_all([1, 2], double)`. The function that receives it is the one that will call it.

<!-- 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: A function as a parameter

Write a function `apply_to_all(numbers, action)` that runs `action` on every number and returns a new list. Use it with a regular function `double`, with a `lambda` that squares, and with `round`.

```python
print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

**Expected output:**

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

{{< solution >}}
```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

Pass the function **without brackets** (`double`, `round`). `apply_to_all` is the one that calls it.
{{< /solution >}}

### Exercise 2: map and filter

```python
prices = [20, 75, 120, 45, 90]
```

1. Keep only prices above 50, using `filter`.
2. Add 10 to every price that remained, using `map`.

**Expected output:**

```text
[75, 120, 90]
[85, 130, 100]
```

{{< solution >}}
```python
prices = [20, 75, 120, 45, 90]

expensive = list(filter(lambda p: p > 50, prices))
raised = list(map(lambda p: p + 10, expensive))
print(expensive)
print(raised)
```

```text
[75, 120, 90]
[85, 130, 100]
```

`filter` keeps only what the lambda returns `True` for. `map` runs the lambda on every item. `list(...)` turns the result into a list.
{{< /solution >}}

### Exercise 3: key — what to sort by

```python
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
```

1. Sort the words by length.
2. Find the longest word.
3. Sort the students by grade, from the highest to the lowest, and print only the names.

**Expected output:**

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

{{< solution >}}
```python
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]

print(sorted(words, key=len))
print(max(words, key=len))

for name, grade in sorted(students, key=lambda s: s[1], reverse=True):
    print(name)
```

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

`key=len` — compare by **the result** of `len` on each word, not by the word itself.
{{< /solution >}}

### Exercise 4: A function that returns a function

Write a function `make_discount(percent)` that returns a function, which receives a price and returns it after the discount.

```python
ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
```

**Expected output:**

```text
180.0
100.0
```

{{< solution >}}
```python
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply


ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
```

```text
180.0
100.0
```

`make_discount` is a "factory" — every call produces a new function that remembers its own `percent`.
{{< /solution >}}

### Exercise 5: Calling instead of passing

```python
def double(n):
    return n * 2

print(apply_to_all([1, 2], double()))
```

1. Which error will you get, and why?
2. What is the fix?

{{< solution >}}
1. `TypeError: double() missing 1 required positional argument: 'n'`. The brackets in `double()` **call** the function immediately — without a parameter — even before `apply_to_all` starts.
2. Pass the function itself, **without brackets**:

```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2], double))
```

```text
[2, 4]
```
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Higher-Order Function** = a function that receives a function or returns a function. It is possible because a function is an object.
- Pass a function **without brackets** — the function that receives it is the one that calls it.
- **`map`** — an action on every item. **`filter`** — keeps what meets the condition.
- **`key=`** in `sorted`, `max`, `min` — sets **what to compare by**.
- A function can **return** a function — a "factory" that produces customized functions.

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

## Official documentation

- [Functional Programming HOWTO — Built-in functions](https://docs.python.org/3/howto/functional.html#built-in-functions)
- [sorted() — key](https://docs.python.org/3/builtins/functions.html#sorted)

<!-- docs:end -->
