# Pure Functions and Side Effects<no value>

## What will we learn?

- What a **Pure Function** is.
- What a **Side Effect** is — and which kinds exist.
- The trap of **changing a list that was received as a parameter**.
- How to write a **pure version** of a function.

## Why do we need this?

Here is a function that looks innocent:

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total

print(add_to_total(10))
print(add_to_total(10))
```

**Output:**

```text
10
20
```

**Exactly the same call — and a different result.** The function depends on an outside variable and also changes it. To know what it will return, you need to know the whole history of the program. It is hard to test, and hard to trust.

## Explanation from the basics

### What a Pure Function is

A **pure function** meets two conditions:

1. **The same input → always the same result.** The result depends only on the parameters — not on global variables, not on the time, not on input from the user.
2. **No Side Effects.** It does not change anything outside it. Everything it "puts out" comes out through `return`.

### What counts as a Side Effect

| Side Effect | Example |
|---|---|
| Changing a global variable | `global total` and then `total += price` |
| Changing a list or a dictionary that was received | `prices[i] = ...` on the list that came in as a parameter |
| Input and output with the user | `print(...)`, `input(...)` |
| Writing to a file or a database | `open(..., "w")`, `INSERT` |

{{< box "analogy" "Analogy: a juice machine" >}}
A pure function is like a juicer: you put in 3 oranges — a glass of juice comes out. Always the same amount, and the juicer touches nothing else in the kitchen. An impure function is a juicer that sometimes also moves the fridge — and suddenly something else in the house does not work, and you have no idea why.
{{< /box >}}

## Syntax

There is no special syntax — it is **a way of writing** functions:

```python
# pure: result depends only on the parameters
def add_price(total, price):
    return total + price

# not pure: reads and changes something outside
def add_to_total(price):
    global total
    total += price
    return total
```

## First code example

The same function from the start of the lesson, in a pure version. `week-2/pure_total.py`:

```python
def add_price(total, price):
    return total + price

total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**Output:**

```text
20
10
10
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `def add_price(total, price):` | Everything the function needs comes **through the parameters**. |
| `return total + price` | It changes nothing — it only calculates and returns. |
| `total = add_price(total, 10)` | **The caller** decides what to do with the result: here — store it in `total`. |
| `print(add_price(0, 10))` twice | The same input → the same result, `10`, every time. |

## Another example

**The trap: changing a list that was received.** `week-2/discount_trap.py`:

```python
def add_discount(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * 0.9
    return prices

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Output:**

```text
[90.0, 180.0]
[90.0, 180.0]
```

**`original` changed!** As we learned in the lesson on Mutability, a list is passed to a function **as a reference** — `prices` and `original` are two names for the same list. Whoever used `original` later in the program suddenly gets different prices.

**The pure version — build a new list:**

```python
def add_discount(prices):
    result = []
    for price in prices:
        result.append(price * 0.9)
    return result

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Output:**

```text
[90.0, 180.0]
[100, 200]
```

Now `original` stays exactly as it was, and `discounted` is a completely new list.

## Common mistake

**The mistake: thinking that every function must be pure.** A program that does not print, does not save to a file and does not write to a database — does nothing useful. Side Effects are **a necessary part** of every program.

The rule is to **separate**:

- **The calculation** — in pure functions (`calculate_total`, `apply_discount`).
- **The communication with the world** — in a few clear places (`print`, saving to a file).

```python
def calculate_total(prices):        # pure: easy to test
    return sum(prices)

def show_total(prices):             # side effect: printing
    print("Total:", calculate_total(prices))
```

<!-- 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: Pure or not?

For each function — is it pure? If not, what is the Side Effect?

```text
def square(n):
    return n * n

def log_square(n):
    print("squaring", n)
    return n * n

def add_item(cart, item):
    cart.append(item)
    return cart

def with_item(cart, item):
    return cart + [item]
```

{{< solution >}}
| Function | Pure? | Why |
|---|---|---|
| `square` | **Yes** | The result depends only on `n`, and nothing changes |
| `log_square` | **No** | `print` is a Side Effect — output to the screen |
| `add_item` | **No** | `append` changes the list that was received (Mutation) |
| `with_item` | **Yes** | `cart + [item]` creates a **new** list, and the original was not touched |
{{< /solution >}}

### Exercise 2: The same call — a different result

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total
```

The problem: every call to `add_to_total(10)` returns a different result (10, then 20, then 30), because the function depends on a global variable and changes it.

Rewrite the function as a pure version named `add_price(total, price)` — it receives the current total as a parameter and returns the new total, without `global`. Run the test code, which shows that the same input always gives the same result:

```python
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**Expected output:**

```text
20
10
10
```

{{< solution >}}
```python
def add_price(total, price):
    return total + price


total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

```text
20
10
10
```

Everything the function needs comes **through the parameters**, and the result comes out only through `return`. The caller decides what to do with it.
{{< /solution >}}

### Exercise 3: Don't change the list you received

```python
TAX = 1.17

def apply_tax(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * TAX
```

The problem: the function changes the list it received, so after the call the original prices are lost.

Write a pure version of `apply_tax(prices)`, which returns a **new** list with the prices including tax (each price rounded to two digits) and does not touch the list it received. Run the test code — the first line proves that the original list did not change:

```python
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

**Expected output:**

```text
[100, 50]
[117.0, 58.5]
```

{{< solution >}}
```python
TAX = 1.17


def apply_tax(prices):
    result = []
    for price in prices:
        result.append(round(price * TAX, 2))
    return result


prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

```text
[100, 50]
[117.0, 58.5]
```

`TAX` is a constant that does not change, so reading it does not hurt the purity — the same input still always gives the same result.
{{< /solution >}}

### Exercise 4: Separating calculation from printing

```python
def show_total(prices):
    total = 0
    for price in prices:
        total = total + price
    print("Total:", total)
```

The function both calculates and prints. Split it into two:

- `calculate_total(prices)` — pure: only calculates and returns the sum;
- `show_total(prices)` — calls `calculate_total` and prints the result.

Test code:

```python
show_total([10, 20, 30])
```

**Expected output:**

```text
Total: 60
```

{{< solution >}}
```python
def calculate_total(prices):
    return sum(prices)


def show_total(prices):
    print("Total:", calculate_total(prices))


show_total([10, 20, 30])
```

```text
Total: 60
```

The calculation (`calculate_total`) is pure and easy to test. The Side Effect (`print`) is concentrated in one clear place.
{{< /solution >}}

### Exercise 5: The top three — without changing anything

Write a pure function `top_three(numbers)` that returns the three largest numbers, from the largest to the smallest, without changing the original list. Also print the original list to prove it did not change.

```python
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

**Expected output:**

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

{{< solution >}}
```python
def top_three(numbers):
    return sorted(numbers, reverse=True)[:3]


scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

`sorted` returns a **new** sorted list, and Slicing (`[:3]`) returns another new list — the original list was not touched, so the function is pure.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Pure Function** = the same input → always the same result, and without changing anything outside.
- **Side Effects**: changing a global, changing a received list or dictionary, `print`/`input`, writing to a file or a database.
- A list is passed as a reference — a function that changes it changes the **original**. The pure version builds a new list.
- Pure functions are easy to test, predictable, and easy to reuse.
- Not everything can be pure — separate the pure **calculation** from the **communication with the world**.

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

## Official documentation

- [Functional Programming HOWTO](https://docs.python.org/3/howto/functional.html#introduction)

<!-- docs:end -->
